Skip to content

Commit

Permalink
jobs: enable downloading execution detail files
Browse files Browse the repository at this point in the history
In cockroachdb#106879 we added a table to the `Advanced Debugging`
tab of the job details page. This table lists out all
the execution detail files that are available for the
given job.

This change is a follow up to add download functionality
to each row in the table. The format of the downloaded
file is determined by the prefix of the filename.

A final change to allow users to generate execution details
will be added in the next follow up.

Informs: cockroachdb#105076
Release note: None
  • Loading branch information
adityamaru committed Jul 27, 2023
1 parent f2e63a4 commit 566f3cb
Show file tree
Hide file tree
Showing 12 changed files with 183 additions and 52 deletions.
19 changes: 18 additions & 1 deletion pkg/ui/workspaces/cluster-ui/src/api/jobProfilerApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ export type ListJobProfilerExecutionDetailsRequest =
export type ListJobProfilerExecutionDetailsResponse =
cockroach.server.serverpb.ListJobProfilerExecutionDetailsResponse;

export const getExecutionDetails = (
export type GetJobProfilerExecutionDetailRequest =
cockroach.server.serverpb.GetJobProfilerExecutionDetailRequest;
export type GetJobProfilerExecutionDetailResponse =
cockroach.server.serverpb.GetJobProfilerExecutionDetailResponse;

export const listExecutionDetailFiles = (
req: ListJobProfilerExecutionDetailsRequest,
): Promise<cockroach.server.serverpb.ListJobProfilerExecutionDetailsResponse> => {
return fetchData(
Expand All @@ -27,3 +32,15 @@ export const getExecutionDetails = (
"30M",
);
};

export const getExecutionDetailFile = (
req: GetJobProfilerExecutionDetailRequest,
): Promise<cockroach.server.serverpb.GetJobProfilerExecutionDetailResponse> => {
return fetchData(
cockroach.server.serverpb.GetJobProfilerExecutionDetailResponse,
`/_status/job_profiler_execution_details/${req.job_id}/${req.filename}`,
null,
null,
"30M",
);
};
22 changes: 9 additions & 13 deletions pkg/ui/workspaces/cluster-ui/src/downloadFile/downloadFile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,13 @@ import React, {
useImperativeHandle,
} from "react";

type FileTypes = "text/plain" | "application/json";

export interface DownloadAsFileProps {
fileName?: string;
fileType?: FileTypes;
content?: string;
content?: Blob;
}

export interface DownloadFileRef {
download: (name: string, type: FileTypes, body: string) => void;
download: (name: string, body: Blob) => void;
}

/*
Expand Down Expand Up @@ -58,26 +55,25 @@ export interface DownloadFileRef {
// tslint:disable-next-line:variable-name
export const DownloadFile = forwardRef<DownloadFileRef, DownloadAsFileProps>(
(props, ref) => {
const { children, fileName, fileType, content } = props;
const { children, fileName, content } = props;
const anchorRef = useRef<HTMLAnchorElement>();

const bootstrapFile = (name: string, type: FileTypes, body: string) => {
const bootstrapFile = (name: string, body: Blob) => {
const anchorElement = anchorRef.current;
const file = new Blob([body], { type });
anchorElement.href = URL.createObjectURL(file);
anchorElement.href = URL.createObjectURL(body);
anchorElement.download = name;
};

useEffect(() => {
if (content === undefined) {
return;
}
bootstrapFile(fileName, fileType, content);
}, [fileName, fileType, content]);
bootstrapFile(fileName, content);
}, [fileName, content]);

useImperativeHandle(ref, () => ({
download: (name: string, type: FileTypes, body: string) => {
bootstrapFile(name, type, body);
download: (name: string, body: Blob) => {
bootstrapFile(name, body);
anchorRef.current.click();
},
}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ import jobStyles from "src/jobs/jobs.module.scss";
import classNames from "classnames/bind";
import { Timestamp } from "../../timestamp";
import {
GetJobProfilerExecutionDetailRequest,
GetJobProfilerExecutionDetailResponse,
ListJobProfilerExecutionDetailsRequest,
ListJobProfilerExecutionDetailsResponse,
RequestState,
Expand All @@ -60,14 +62,17 @@ enum TabKeysEnum {

export interface JobDetailsStateProps {
jobRequest: RequestState<JobResponse>;
jobProfilerResponse: RequestState<ListJobProfilerExecutionDetailsResponse>;
jobProfilerExecutionDetailFilesResponse: RequestState<ListJobProfilerExecutionDetailsResponse>;
jobProfilerLastUpdated: moment.Moment;
jobProfilerDataIsValid: boolean;
onDownloadExecutionFileClicked: (
req: GetJobProfilerExecutionDetailRequest,
) => Promise<GetJobProfilerExecutionDetailResponse>;
}

export interface JobDetailsDispatchProps {
refreshJob: (req: JobRequest) => void;
refreshExecutionDetails: (
refreshExecutionDetailFiles: (
req: ListJobProfilerExecutionDetailsRequest,
) => void;
}
Expand Down Expand Up @@ -130,10 +135,15 @@ export class JobDetails extends React.Component<
return (
<JobProfilerView
jobID={id}
executionDetailsResponse={this.props.jobProfilerResponse}
refreshExecutionDetails={this.props.refreshExecutionDetails}
executionDetailFilesResponse={
this.props.jobProfilerExecutionDetailFilesResponse
}
refreshExecutionDetailFiles={this.props.refreshExecutionDetailFiles}
lastUpdated={this.props.jobProfilerLastUpdated}
isDataValid={this.props.jobProfilerDataIsValid}
onDownloadExecutionFileClicked={
this.props.onDownloadExecutionFileClicked
}
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { selectID } from "../../selectors";
import {
ListJobProfilerExecutionDetailsRequest,
createInitialState,
getExecutionDetailFile,
} from "src/api";
import {
initialState,
Expand All @@ -39,15 +40,17 @@ const mapStateToProps = (
const jobID = selectID(state, props);
return {
jobRequest: state.adminUI?.job?.cachedData[jobID] ?? emptyState,
jobProfilerResponse: state.adminUI?.executionDetails ?? initialState,
jobProfilerLastUpdated: state.adminUI?.executionDetails?.lastUpdated,
jobProfilerDataIsValid: state.adminUI?.executionDetails?.valid,
jobProfilerExecutionDetailFilesResponse:
state.adminUI?.executionDetailFiles ?? initialState,
jobProfilerLastUpdated: state.adminUI?.executionDetailFiles?.lastUpdated,
jobProfilerDataIsValid: state.adminUI?.executionDetailFiles?.valid,
onDownloadExecutionFileClicked: getExecutionDetailFile,
};
};

const mapDispatchToProps = (dispatch: Dispatch): JobDetailsDispatchProps => ({
refreshJob: (req: JobRequest) => jobActions.refresh(req),
refreshExecutionDetails: (req: ListJobProfilerExecutionDetailsRequest) =>
refreshExecutionDetailFiles: (req: ListJobProfilerExecutionDetailsRequest) =>
dispatch(jobProfilerActions.refresh(req)),
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
@import "src/core/index.module";

.crl-job-profiler-view {
&__actions-column {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-end;
}
}

.column-size-medium {
width: 230px;
}

.download-execution-detail-button {
white-space: nowrap;

>svg {
margin-right: $spacing-x-small;
}
}

.sorted-table {
width: 100%;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,8 @@
import { cockroach } from "@cockroachlabs/crdb-protobuf-client";
import moment from "moment-timezone";
import React, { useCallback, useEffect, useState } from "react";
import {
RequestState,
ListJobProfilerExecutionDetailsResponse,
ListJobProfilerExecutionDetailsRequest,
} from "src/api";
import { InlineAlert } from "@cockroachlabs/ui-components";
import { RequestState } from "src/api";
import { Button, InlineAlert, Icon } from "@cockroachlabs/ui-components";
import { Row, Col } from "antd";
import "antd/lib/col/style";
import "antd/lib/row/style";
Expand All @@ -29,45 +25,129 @@ import classnames from "classnames/bind";
import styles from "./jobProfilerView.module.scss";
import { EmptyTable } from "src/empty";
import { useScheduleFunction } from "src/util/hooks";
import { DownloadFile, DownloadFileRef } from "src/downloadFile";
import {
GetJobProfilerExecutionDetailRequest,
GetJobProfilerExecutionDetailResponse,
ListJobProfilerExecutionDetailsRequest,
ListJobProfilerExecutionDetailsResponse,
} from "src/api/jobProfilerApi";

const cardCx = classNames.bind(summaryCardStyles);
const cx = classnames.bind(styles);

export type JobProfilerStateProps = {
jobID: long;
executionDetailsResponse: RequestState<ListJobProfilerExecutionDetailsResponse>;
executionDetailFilesResponse: RequestState<ListJobProfilerExecutionDetailsResponse>;
lastUpdated: moment.Moment;
isDataValid: boolean;
onDownloadExecutionFileClicked: (
req: GetJobProfilerExecutionDetailRequest,
) => Promise<GetJobProfilerExecutionDetailResponse>;
};

export type JobProfilerDispatchProps = {
refreshExecutionDetails: (
refreshExecutionDetailFiles: (
req: ListJobProfilerExecutionDetailsRequest,
) => void;
};

export type JobProfilerViewProps = JobProfilerStateProps &
JobProfilerDispatchProps;

export function makeJobProfilerViewColumns(): ColumnDescriptor<string>[] {
export function extractFileExtension(filename: string): string {
const parts = filename.split(".");
// The extension is the last part after the last dot (if it exists).
return parts.length > 1 ? parts[parts.length - 1] : "";
}

export function getContentTypeForFile(filename: string): string {
const extension = extractFileExtension(filename);
switch (extension) {
case "txt":
return "text/plain";
case "zip":
return "application/zip";
case "html":
return "text/html";
default:
return "";
}
}

export function makeJobProfilerViewColumns(
jobID: Long,
onDownloadExecutionFileClicked: (
req: GetJobProfilerExecutionDetailRequest,
) => Promise<GetJobProfilerExecutionDetailResponse>,
): ColumnDescriptor<string>[] {
const downloadRef: React.RefObject<DownloadFileRef> =
React.createRef<DownloadFileRef>();
return [
{
name: "executionDetailFiles",
title: "Execution Detail Files",
hideTitleUnderline: true,
cell: (executionDetails: string) => executionDetails,
},
{
name: "actions",
title: "",
hideTitleUnderline: true,
className: cx("column-size-medium"),
cell: (executionDetailFile: string) => {
return (
<div className={cx("crl-job-profiler-view__actions-column")}>
<DownloadFile ref={downloadRef} />
<Button
as="a"
size="small"
intent="tertiary"
className={cx("download-execution-detail-button")}
onClick={() => {
const req =
new cockroach.server.serverpb.GetJobProfilerExecutionDetailRequest(
{
job_id: jobID,
filename: executionDetailFile,
},
);
onDownloadExecutionFileClicked(req).then(resp => {
const type = getContentTypeForFile(executionDetailFile);
const executionFileBytes = new Blob([resp.data], {
type: type,
});
Promise.resolve().then(() => {
downloadRef.current.download(
executionDetailFile,
executionFileBytes,
);
});
});
}}
>
<Icon iconName="Download" />
Download
</Button>
</div>
);
},
},
];
}

export const JobProfilerView: React.FC<JobProfilerViewProps> = ({
jobID,
executionDetailsResponse,
executionDetailFilesResponse,
lastUpdated,
isDataValid,
refreshExecutionDetails,
onDownloadExecutionFileClicked,
refreshExecutionDetailFiles,
}: JobProfilerViewProps) => {
const columns = makeJobProfilerViewColumns();
const columns = makeJobProfilerViewColumns(
jobID,
onDownloadExecutionFileClicked,
);
const [sortSetting, setSortSetting] = useState<SortSetting>({
ascending: true,
columnTitle: "executionDetailFiles",
Expand All @@ -77,8 +157,8 @@ export const JobProfilerView: React.FC<JobProfilerViewProps> = ({
job_id: jobID,
});
const refresh = useCallback(() => {
refreshExecutionDetails(req);
}, [refreshExecutionDetails, req]);
refreshExecutionDetailFiles(req);
}, [refreshExecutionDetailFiles, req]);
const [refetch] = useScheduleFunction(
refresh,
true,
Expand Down Expand Up @@ -119,7 +199,7 @@ export const JobProfilerView: React.FC<JobProfilerViewProps> = ({
<Row gutter={24}>
<Col className="gutter-row" span={24}>
<SortedTable
data={executionDetailsResponse.data?.files}
data={executionDetailFilesResponse.data?.files}
columns={columns}
tableWrapperClassName={cx("sorted-table")}
sortSetting={sortSetting}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
ListJobProfilerExecutionDetailsResponse,
} from "src/api";

export type JobProfilerState =
export type JobProfilerExecutionDetailFilesState =
RequestState<ListJobProfilerExecutionDetailsResponse>;

export const initialState =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { actions } from "./jobProfiler.reducer";
import { call, put, all, takeEvery } from "redux-saga/effects";
import {
ListJobProfilerExecutionDetailsRequest,
getExecutionDetails,
listExecutionDetailFiles,
} from "src/api";

export function* refreshJobProfilerSaga(
Expand All @@ -26,7 +26,7 @@ export function* requestJobProfilerSaga(
action: PayloadAction<ListJobProfilerExecutionDetailsRequest>,
): any {
try {
const result = yield call(getExecutionDetails, action.payload);
const result = yield call(listExecutionDetailFiles, action.payload);
yield put(actions.received(result));
} catch (e) {
yield put(actions.failed(e));
Expand Down
Loading

0 comments on commit 566f3cb

Please sign in to comment.