Skip to content

Commit

Permalink
Merge #83901
Browse files Browse the repository at this point in the history
83901: ui: add connected component for jobs pages r=ericharmeling a=ericharmeling

This PR adds "connected" components for the jobs pages. These components can be imported from the `cluster-ui` package into the `managed-service` repo and used on the CC console **for Dedicated clusters** (serverless clusters do not support the admin server that handles job requests).

Related to #71324.

Release note: None

Co-authored-by: Eric Harmeling <[email protected]>
  • Loading branch information
craig[bot] and ericharmeling committed Jul 11, 2022
2 parents 86e007d + 046d6e6 commit 37cf9bf
Show file tree
Hide file tree
Showing 28 changed files with 775 additions and 58 deletions.
33 changes: 33 additions & 0 deletions pkg/ui/workspaces/cluster-ui/src/api/jobsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,42 @@
// licenses/APL.txt.

import { cockroach } from "@cockroachlabs/crdb-protobuf-client";
import { fetchData } from "./fetchData";
import { propsToQueryString } from "../util";

const JOBS_PATH = "/_admin/v1/jobs";

export type JobsRequest = cockroach.server.serverpb.JobsRequest;
export type JobsResponse = cockroach.server.serverpb.JobsResponse;

export type JobRequest = cockroach.server.serverpb.JobRequest;
export type JobResponse = cockroach.server.serverpb.JobResponse;

export const getJobs = (
req: JobsRequest,
): Promise<cockroach.server.serverpb.JobsResponse> => {
const queryStr = propsToQueryString({
status: req.status,
type: req.type.toString(),
limit: req.limit,
});
return fetchData(
cockroach.server.serverpb.JobsResponse,
`${JOBS_PATH}?${queryStr}`,
null,
null,
"30M",
);
};

export const getJob = (
req: JobRequest,
): Promise<cockroach.server.serverpb.JobResponse> => {
return fetchData(
cockroach.server.serverpb.JobResponse,
`${JOBS_PATH}/${req.job_id}`,
null,
null,
"30M",
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
// licenses/APL.txt.

export * from "./jobDetails";
export * from "./jobDetailsConnected";
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright 2022 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

import { connect } from "react-redux";
import { RouteComponentProps, withRouter } from "react-router-dom";

import { AppState } from "src/store";
import { selectJobState } from "../../store/jobDetails/job.selectors";
import {
JobDetailsStateProps,
JobDetailsDispatchProps,
JobDetails,
} from "./jobDetails";
import { JobRequest } from "src/api/jobsApi";
import { actions as jobActions } from "src/store/jobDetails";

const mapStateToProps = (state: AppState): JobDetailsStateProps => {
const jobState = selectJobState(state);
const job = jobState ? jobState.data : null;
const jobLoading = jobState ? jobState.inFlight : false;
const jobError = jobState ? jobState.lastError : null;
return {
job,
jobLoading,
jobError,
};
};

const mapDispatchToProps = {
refreshJob: (req: JobRequest) => jobActions.refresh(req),
};

export const JobDetailsPageConnected = withRouter(
connect<JobDetailsStateProps, JobDetailsDispatchProps, RouteComponentProps>(
mapStateToProps,
mapDispatchToProps,
)(JobDetails),
);
1 change: 1 addition & 0 deletions pkg/ui/workspaces/cluster-ui/src/jobs/jobsPage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@
export * from "./jobDescriptionCell";
export * from "./jobsPage";
export * from "./jobTable";
export * from "./jobsPageConnected";
15 changes: 10 additions & 5 deletions pkg/ui/workspaces/cluster-ui/src/jobs/jobsPage/jobTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import { cockroach, google } from "@cockroachlabs/crdb-protobuf-client";
import { Tooltip } from "@cockroachlabs/ui-components";
import { isEqual, map } from "lodash";
import React, { MouseEvent } from "react";
import React from "react";
import { Anchor } from "src/anchor";
import { JobsResponse } from "src/api/jobsApi";
import emptyTableResultsIcon from "src/assets/emptyState/empty-table-results.svg";
Expand All @@ -28,9 +28,8 @@ import {
} from "src/util/docs";
import { DATE_FORMAT_24_UTC } from "src/util/format";

import { HighwaterTimestamp } from "../util/highwaterTimestamp";
import { HighwaterTimestamp, JobStatusCell } from "../util";
import { JobDescriptionCell } from "./jobDescriptionCell";
import { JobStatusCell } from "../util/jobStatusCell";

import styles from "../jobs.module.scss";
import classNames from "classnames/bind";
Expand Down Expand Up @@ -125,7 +124,7 @@ const jobsTableColumns: ColumnDescriptor<Job>[] = [
style="tableTitle"
content={<p>User that created the job.</p>}
>
{"User"}
{"User Name"}
</Tooltip>
),
cell: job => job.username,
Expand Down Expand Up @@ -179,7 +178,13 @@ const jobsTableColumns: ColumnDescriptor<Job>[] = [
<Tooltip
placement="bottom"
style="tableTitle"
content={<p>Date and time the job was last executed.</p>}
content={
<p>
The high-water mark acts as a checkpoint for the changefeed’s job
progress, and guarantees that all changes before (or at) the
timestamp have been emitted.
</p>
}
>
{"High-water Timestamp"}
</Tooltip>
Expand Down
13 changes: 4 additions & 9 deletions pkg/ui/workspaces/cluster-ui/src/jobs/jobsPage/jobsPage.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,13 @@
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

import { assert, expect } from "chai";
import { assert } from "chai";
import moment from "moment";
import { cockroach } from "@cockroachlabs/crdb-protobuf-client";
import { JobsPage, JobsPageProps } from "./jobsPage";
import { formatDuration } from "../util/duration";
import {
allJobsFixture,
retryRunningJobFixture,
earliestRetainedTime,
} from "./jobsPage.fixture";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { allJobsFixture, earliestRetainedTime } from "./jobsPage.fixture";
import { render } from "@testing-library/react";
import React from "react";
import { MemoryRouter } from "react-router-dom";
import * as H from "history";
Expand Down Expand Up @@ -78,7 +73,7 @@ describe("Jobs", () => {
"Description",
"Status",
"Job ID",
"User",
"User Name",
"Creation Time (UTC)",
"Last Execution Time (UTC)",
"Execution Count",
Expand Down
30 changes: 16 additions & 14 deletions pkg/ui/workspaces/cluster-ui/src/jobs/jobsPage/jobsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { SortSetting } from "src/sortedtable";
import { syncHistory } from "src/util";

import { JobTable } from "./jobTable";
import { statusOptions, showOptions, typeOptions } from "../util/jobOptions";
import { statusOptions, showOptions, typeOptions } from "../util";

import { commonStyles } from "src/common";
import styles from "../jobs.module.scss";
Expand All @@ -48,11 +48,12 @@ export interface JobsPageDispatchProps {
setShow: (value: string) => void;
setType: (value: JobType) => void;
refreshJobs: (req: JobsRequest) => void;
onFilterChange?: (req: JobsRequest) => void;
}

export type JobsPageProps = JobsPageStateProps &
JobsPageDispatchProps &
RouteComponentProps<unknown>;
RouteComponentProps;

export class JobsPage extends React.Component<JobsPageProps> {
constructor(props: JobsPageProps) {
Expand All @@ -68,8 +69,8 @@ export class JobsPage extends React.Component<JobsPageProps> {
if (
this.props.setSort &&
columnTitle &&
(sortSetting.columnTitle != columnTitle ||
sortSetting.ascending != ascending)
(sortSetting.columnTitle !== columnTitle ||
sortSetting.ascending !== ascending)
) {
this.props.setSort({ columnTitle, ascending });
}
Expand All @@ -82,25 +83,26 @@ export class JobsPage extends React.Component<JobsPageProps> {

// Filter Show.
const show = searchParams.get("show") || undefined;
if (this.props.setShow && show && show != this.props.show) {
if (this.props.setShow && show && show !== this.props.show) {
this.props.setShow(show);
}

// Filter Type.
const type = parseInt(searchParams.get("type"), 10) || undefined;
if (this.props.setType && type && type != this.props.type) {
if (this.props.setType && type && type !== this.props.type) {
this.props.setType(type);
}
}

private refresh(props = this.props): void {
props.refreshJobs(
new cockroach.server.serverpb.JobsRequest({
status: props.status,
type: props.type,
limit: parseInt(props.show, 10),
}),
);
const jobsRequest = new cockroach.server.serverpb.JobsRequest({
status: props.status,
type: props.type,
limit: parseInt(props.show, 10),
});
props.onFilterChange
? props.onFilterChange(jobsRequest)
: props.refreshJobs(jobsRequest);
}

componentDidMount(): void {
Expand All @@ -113,7 +115,7 @@ export class JobsPage extends React.Component<JobsPageProps> {
prevProps.type !== this.props.type ||
prevProps.show !== this.props.show
) {
this.refresh(this.props);
this.refresh();
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright 2022 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

import { connect } from "react-redux";
import { RouteComponentProps, withRouter } from "react-router-dom";

import { AppState } from "src/store";
import {
selectJobsState,
selectShowSetting,
selectSortSetting,
selectTypeSetting,
selectStatusSetting,
} from "../../store/jobs/jobs.selectors";
import {
JobsPageStateProps,
JobsPageDispatchProps,
JobsPage,
} from "./jobsPage";
import { JobsRequest } from "src/api/jobsApi";
import { actions as jobsActions } from "src/store/jobs";
import { actions as localStorageActions } from "../../store/localStorage";
import { Dispatch } from "redux";
import { SortSetting } from "../../sortedtable";

const mapStateToProps = (
state: AppState,
_: RouteComponentProps,
): JobsPageStateProps => {
const sort = selectSortSetting(state);
const status = selectStatusSetting(state);
const show = selectShowSetting(state);
const type = selectTypeSetting(state);
const jobsState = selectJobsState(state);
const jobs = jobsState ? jobsState.data : null;
const jobsLoading = jobsState ? jobsState.inFlight : false;
const jobsError = jobsState ? jobsState.lastError : null;
return {
sort,
status,
show,
type,
jobs,
jobsLoading,
jobsError,
};
};

const mapDispatchToProps = (dispatch: Dispatch): JobsPageDispatchProps => ({
setShow: (showValue: string) => {
dispatch(
localStorageActions.update({
key: "showSetting/JobsPage",
value: showValue,
}),
);
},
setSort: (ss: SortSetting) => {
dispatch(
localStorageActions.update({
key: "sortSetting/JobsPage",
value: ss,
}),
);
},
setStatus: (statusValue: string) => {
dispatch(
localStorageActions.update({
key: "statusSetting/JobsPage",
value: statusValue,
}),
);
},
setType: (jobValue: number) => {
dispatch(
localStorageActions.update({
key: "typeSetting/JobsPage",
value: jobValue,
}),
);
},
refreshJobs: (req: JobsRequest) => dispatch(jobsActions.refresh(req)),
onFilterChange: (req: JobsRequest) =>
dispatch(jobsActions.updateFilteredJobs(req)),
});

export const JobsPageConnected = withRouter(
connect<JobsPageStateProps, JobsPageDispatchProps, RouteComponentProps>(
mapStateToProps,
mapDispatchToProps,
)(JobsPage),
);
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,13 @@ import {
import { Dispatch } from "redux";
import { Filters } from "../queryFilter";
import { sqlStatsSelector } from "../store/sqlStats/sqlStats.selector";
import { localStorageSelector } from "../store/utils/selectors";

export const selectSessionsData = createSelector(
sqlStatsSelector,
sessionsState => (sessionsState.valid ? sessionsState.data : null),
);

export const adminUISelector = createSelector(
(state: AppState) => state.adminUI,
adminUiState => adminUiState,
);

export const localStorageSelector = createSelector(
adminUISelector,
adminUiState => adminUiState.localStorage,
);

export const selectSessions = createSelector(
(state: AppState) => state.adminUI.sessions,
(state: SessionsState) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import { createSelector } from "reselect";
import { getActiveStatementsFromSessions } from "../activeExecutions/activeStatementUtils";
import { localStorageSelector } from "./statementsPage.selectors";
import { localStorageSelector } from "../store/utils/selectors";
import {
ActiveStatementFilters,
ActiveStatementsViewDispatchProps,
Expand Down
Loading

0 comments on commit 37cf9bf

Please sign in to comment.