Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
86409: ui: Add page to view schedules in DB console. r=benbardin a=benbardin

*Summary*
Adds /schedules page in DB console. Structure is very similar to /jobs page. The new /schedules page provides a bit more observability into what schedules are running, and their various states.

Release note (ui change): Add page to view schedules in DB console.
Release justification: Low-risk - new, read-only page.
See also cockroachdb#70725

*Artifacts*
Loom link: https://www.loom.com/share/0fb4fee5859d4a46a3b6442892ae9ef6

<img width="1277" alt="Screen Shot 2022-08-18 at 2 23 40 PM" src="https://user-images.githubusercontent.com/261508/185491986-3063e9b5-af0f-41c5-bd69-1b7b6a9a871b.png">
<img width="1276" alt="Screen Shot 2022-08-18 at 2 23 14 PM" src="https://user-images.githubusercontent.com/261508/185491988-dba19a72-f1d6-4d74-9529-370d5012a0ad.png">
<img width="1260" alt="Screen Shot 2022-08-18 at 2 36 39 PM" src="https://user-images.githubusercontent.com/261508/185491985-3fd8293b-e6e1-4e6c-9f90-31b31b988677.png">
<img width="1257" alt="Screen Shot 2022-08-18 at 2 23 20 PM" src="https://user-images.githubusercontent.com/261508/185491987-fa5dc38e-1f8b-48b2-9c8f-39926db70e27.png">


Co-authored-by: Ben Bardin <[email protected]>
  • Loading branch information
craig[bot] and benbardin committed Aug 26, 2022
2 parents 8c84c34 + 03fde0e commit 4471d8b
Show file tree
Hide file tree
Showing 23 changed files with 1,528 additions and 2 deletions.
1 change: 1 addition & 0 deletions pkg/ui/workspaces/cluster-ui/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ export * from "./clusterLocksApi";
export * from "./insightsApi";
export * from "./indexActionsApi";
export * from "./schemaInsightsApi";
export * from "./schedulesApi";
153 changes: 153 additions & 0 deletions pkg/ui/workspaces/cluster-ui/src/api/schedulesApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// 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 Long from "long";
import moment from "moment";
import { executeSql, SqlExecutionRequest } from "./sqlApi";
import { RequestError } from "../util";

type ScheduleColumns = {
id: string;
label: string;
schedule_status: string;
next_run: string;
state: string;
recurrence: string;
jobsrunning: number;
owner: string;
created: string;
command: string;
};

export type Schedule = {
id: Long;
label: string;
status: string;
nextRun?: moment.Moment;
state: string;
recurrence: string;
jobsRunning: number;
owner: string;
created: moment.Moment;
command: string;
};

export type Schedules = Schedule[];

export function getSchedules(req: {
status: string;
limit: number;
}): Promise<Schedules> {
// Cast int64 to string, since otherwise it gets truncated.
// Likewise, prettify `command` on the server since contained int64s
// may also be truncated.
let stmt = `
WITH schedules AS (SHOW SCHEDULES)
SELECT id::string, label, schedule_status, next_run,
state, recurrence, jobsrunning, owner,
created, jsonb_pretty(command) as command
FROM schedules
`;
const args = [];
if (req.status) {
stmt += " WHERE schedule_status = $" + (args.length + 1);
args.push(req.status);
}
stmt += " ORDER BY created DESC";
if (req.limit) {
stmt += " LIMIT $" + (args.length + 1);
args.push(req.limit.toString());
}
const request: SqlExecutionRequest = {
statements: [
{
sql: stmt,
arguments: args,
},
],
execute: true,
};
return executeSql<ScheduleColumns>(request).then(result => {
const txn_results = result.execution.txn_results;
if (txn_results.length === 0 || !txn_results[0].rows) {
// No data.
return [];
}

return txn_results[0].rows.map(row => {
return {
id: Long.fromString(row.id),
label: row.label,
status: row.schedule_status,
nextRun: row.next_run ? moment.utc(row.next_run) : null,
state: row.state,
recurrence: row.recurrence,
jobsRunning: row.jobsrunning,
owner: row.owner,
created: moment.utc(row.created),
command: JSON.parse(row.command),
};
});
});
}

export function getSchedule(id: Long): Promise<Schedule> {
const request: SqlExecutionRequest = {
statements: [
{
// Cast int64 to string, since otherwise it gets truncated.
// Likewise, prettify `command` on the server since contained int64s
// may also be truncated.
sql: `
WITH schedules AS (SHOW SCHEDULES)
SELECT id::string, label, schedule_status, next_run,
state, recurrence, jobsrunning, owner,
created, jsonb_pretty(command) as command
FROM schedules
WHERE ID = $1::int64
`,
arguments: [id.toString()],
},
],
execute: true,
};
return executeSql<ScheduleColumns>(request).then(result => {
const txn_results = result.execution.txn_results;
if (txn_results.length === 0 || !txn_results[0].rows) {
// No data.
throw new RequestError(
"Bad Request",
400,
"No schedule found with this ID.",
);
}

if (txn_results[0].rows.length > 1) {
throw new RequestError(
"Internal Server Error",
500,
"Multiple schedules found for ID.",
);
}
const row = txn_results[0].rows[0];
return {
id: Long.fromString(row.id),
label: row.label,
status: row.schedule_status,
nextRun: row.next_run ? moment.utc(row.next_run) : null,
state: row.state,
recurrence: row.recurrence,
jobsRunning: row.jobsrunning,
owner: row.owner,
created: moment.utc(row.created),
command: row.command,
};
});
}
1 change: 1 addition & 0 deletions pkg/ui/workspaces/cluster-ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export * from "./pageConfig";
export * from "./pagination";
export * from "./queryFilter";
export * from "./search";
export * from "./schedules";
export * from "./sortedtable";
export * from "./statementsDiagnostics";
export * from "./statementsPage";
Expand Down
12 changes: 12 additions & 0 deletions pkg/ui/workspaces/cluster-ui/src/schedules/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// 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.

export * from "./schedulesPage";
export * from "./scheduleDetailsPage";
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// 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.

export * from "./scheduleDetails";
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// 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 { ArrowLeft } from "@cockroachlabs/icons";
import { Col, Row } from "antd";
import "antd/lib/col/style";
import "antd/lib/row/style";
import Long from "long";
import React, { useEffect } from "react";
import Helmet from "react-helmet";
import { RouteComponentProps } from "react-router-dom";
import { Schedule } from "src/api/schedulesApi";
import { Button } from "src/button";
import { Loading } from "src/loading";
import { SqlBox, SqlBoxSize } from "src/sql";
import { SummaryCard, SummaryCardItem } from "src/summaryCard";
import { DATE_FORMAT_24_UTC } from "src/util/format";
import { getMatchParamByName } from "src/util/query";

import { commonStyles } from "src/common";
import summaryCardStyles from "src/summaryCard/summaryCard.module.scss";
import scheduleStyles from "src/schedules/schedules.module.scss";

import classNames from "classnames/bind";

const cardCx = classNames.bind(summaryCardStyles);
const scheduleCx = classNames.bind(scheduleStyles);

export interface ScheduleDetailsStateProps {
schedule: Schedule;
scheduleError: Error | null;
scheduleLoading: boolean;
}

export interface ScheduleDetailsDispatchProps {
refreshSchedule: (id: Long) => void;
}

export type ScheduleDetailsProps = ScheduleDetailsStateProps &
ScheduleDetailsDispatchProps &
RouteComponentProps<unknown>;

export const ScheduleDetails: React.FC<ScheduleDetailsProps> = props => {
const idStr = getMatchParamByName(props.match, "id");
const { refreshSchedule } = props;
useEffect(() => {
refreshSchedule(Long.fromString(idStr));
}, [idStr, refreshSchedule]);

const prevPage = (): void => props.history.goBack();

const renderContent = (): React.ReactElement => {
const schedule = props.schedule;
return (
<>
<Row gutter={24}>
<Col className="gutter-row" span={24}>
<SqlBox value={schedule.command} size={SqlBoxSize.custom} />
</Col>
</Row>
<Row gutter={24}>
<Col className="gutter-row" span={12}>
<SummaryCard>
<SummaryCardItem label="Label" value={schedule.label} />
<SummaryCardItem label="Status" value={schedule.status} />
<SummaryCardItem label="State" value={schedule.state} />
</SummaryCard>
</Col>
<Col className="gutter-row" span={12}>
<SummaryCard className={cardCx("summary-card")}>
<SummaryCardItem
label="Creation Time"
value={schedule.created?.format(DATE_FORMAT_24_UTC)}
/>
<SummaryCardItem
label="Next Execution Time"
value={schedule.nextRun?.format(DATE_FORMAT_24_UTC)}
/>
<SummaryCardItem label="Recurrence" value={schedule.recurrence} />
<SummaryCardItem
label="Jobs Running"
value={String(schedule.jobsRunning)}
/>
<SummaryCardItem label="Owner" value={schedule.owner} />
</SummaryCard>
</Col>
</Row>
</>
);
};

return (
<div className={scheduleCx("schedule-details")}>
<Helmet title={"Details | Schedule"} />
<div className={scheduleCx("section page--header")}>
<Button
onClick={prevPage}
type="unstyled-link"
size="small"
icon={<ArrowLeft fontSize={"10px"} />}
iconPosition="left"
className={commonStyles("small-margin")}
>
Schedules
</Button>
<h3
className={scheduleCx("page--header__title")}
>{`Schedule ID: ${idStr}`}</h3>
</div>
<section className={scheduleCx("section section--container")}>
<Loading
loading={!props.schedule || props.scheduleLoading}
page={"schedule details"}
error={props.scheduleError}
render={renderContent}
/>
</section>
</div>
);
};
Loading

0 comments on commit 4471d8b

Please sign in to comment.