-
Notifications
You must be signed in to change notification settings - Fork 881
feat: workspace view for schedules #991
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { Story } from "@storybook/react" | ||
import React from "react" | ||
import { MockWorkspaceAutostartEnabled } from "../../test_helpers" | ||
import { WorkspaceSchedule, WorkspaceScheduleProps } from "./WorkspaceSchedule" | ||
|
||
export default { | ||
title: "Workspaces/WorkspaceSchedule", | ||
component: WorkspaceSchedule, | ||
} | ||
|
||
const Template: Story<WorkspaceScheduleProps> = (args) => <WorkspaceSchedule {...args} /> | ||
|
||
export const Example = Template.bind({}) | ||
Example.args = { | ||
autostart: MockWorkspaceAutostartEnabled.schedule, | ||
autostop: "", | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import Box from "@material-ui/core/Box" | ||
import Typography from "@material-ui/core/Typography" | ||
import cronstrue from "cronstrue" | ||
import React from "react" | ||
import { expandScheduleCronString, extractTimezone } from "../../util/schedule" | ||
import { WorkspaceSection } from "./WorkspaceSection" | ||
|
||
const Language = { | ||
autoStartLabel: (schedule: string): string => { | ||
const prefix = "Workspace start" | ||
|
||
if (schedule) { | ||
return `${prefix} (${extractTimezone(schedule)})` | ||
} else { | ||
return prefix | ||
} | ||
}, | ||
autoStopLabel: (schedule: string): string => { | ||
const prefix = "Workspace shutdown" | ||
|
||
if (schedule) { | ||
return `${prefix} (${extractTimezone(schedule)})` | ||
} else { | ||
return prefix | ||
} | ||
}, | ||
cronHumanDisplay: (schedule: string): string => { | ||
if (schedule) { | ||
return cronstrue.toString(expandScheduleCronString(schedule), { throwExceptionOnParseError: false }) | ||
} | ||
return "Manual" | ||
}, | ||
} | ||
|
||
export interface WorkspaceScheduleProps { | ||
autostart: string | ||
autostop: string | ||
} | ||
|
||
/** | ||
* WorkspaceSchedule displays a workspace schedule in a human-readable format | ||
* | ||
* @remarks Visual Component | ||
*/ | ||
export const WorkspaceSchedule: React.FC<WorkspaceScheduleProps> = ({ autostart, autostop }) => { | ||
return ( | ||
<WorkspaceSection title="Workspace schedule"> | ||
<Box mt={2}> | ||
<Typography variant="h6">{Language.autoStartLabel(autostart)}</Typography> | ||
<Typography>{Language.cronHumanDisplay(autostart)}</Typography> | ||
</Box> | ||
|
||
<Box mt={2}> | ||
<Typography variant="h6">{Language.autoStopLabel(autostop)}</Typography> | ||
<Typography>{Language.cronHumanDisplay(autostop)}</Typography> | ||
</Box> | ||
</WorkspaceSection> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { expandScheduleCronString, extractTimezone, stripTimezone } from "./schedule" | ||
|
||
describe("util/schedule", () => { | ||
describe("stripTimezone", () => { | ||
it.each<[string, string]>([ | ||
["CRON_TZ=Canada/Eastern 30 9 1-5", "30 9 1-5"], | ||
["CRON_TZ=America/Central 0 8 1,2,4,5", "0 8 1,2,4,5"], | ||
["30 9 1-5", "30 9 1-5"], | ||
])(`stripTimezone(%p) returns %p`, (input, expected) => { | ||
expect(stripTimezone(input)).toBe(expected) | ||
}) | ||
}) | ||
|
||
describe("extractTimezone", () => { | ||
it.each<[string, string]>([ | ||
["CRON_TZ=Canada/Eastern 30 9 1-5", "Canada/Eastern"], | ||
["CRON_TZ=America/Central 0 8 1,2,4,5", "America/Central"], | ||
["30 9 1-5", "UTC"], | ||
])(`extractTimezone(%p) returns %p`, (input, expected) => { | ||
expect(extractTimezone(input)).toBe(expected) | ||
}) | ||
}) | ||
|
||
describe("expandScheduleCronString", () => { | ||
it.each<[string, string]>([ | ||
["CRON_TZ=Canada/Eastern 30 9 1-5", "30 9 * * 1-5"], | ||
["CRON_TZ=America/Central 0 8 1,2,4,5", "0 8 * * 1,2,4,5"], | ||
["30 9 1-5", "30 9 * * 1-5"], | ||
])(`expandScheduleCronString(%p) returns %p`, (input, expected) => { | ||
expect(expandScheduleCronString(input)).toBe(expected) | ||
}) | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
/** | ||
* @fileoverview Client-side counterpart of the coderd/autostart/schedule Go | ||
* package. This package is a variation on crontab that uses minute, hour and | ||
* day of week. | ||
*/ | ||
|
||
/** | ||
* DEFAULT_TIMEZONE is the default timezone that crontab assumes unless one is | ||
* specified. | ||
*/ | ||
const DEFAULT_TIMEZONE = "UTC" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
|
||
/** | ||
* stripTimezone strips a leading timezone from a schedule string | ||
*/ | ||
export const stripTimezone = (raw: string): string => { | ||
return raw.replace(/CRON_TZ=\S*\s/, "") | ||
} | ||
|
||
/** | ||
* extractTimezone returns a leading timezone from a schedule string if one is | ||
* specified; otherwise DEFAULT_TIMEZONE | ||
*/ | ||
export const extractTimezone = (raw: string): string => { | ||
const matches = raw.match(/CRON_TZ=\S*\s/g) | ||
|
||
if (matches && matches.length) { | ||
return matches[0].replace(/CRON_TZ=/, "").trim() | ||
} else { | ||
return DEFAULT_TIMEZONE | ||
} | ||
} | ||
|
||
/** | ||
* expandScheduleCronString ensures a Schedule is expanded to a valid 5-value | ||
* cron string by inserting '*' in month and day positions. If there is a | ||
* leading timezone, it is removed. | ||
* | ||
* @example | ||
* expandScheduleCronString("30 9 1-5") // -> "30 9 * * 1-5" | ||
*/ | ||
export const expandScheduleCronString = (schedule: string): string => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (non-blocking): Would it simplify the frontend code to alter the backend to support a full valid 5-value cron string? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good question, hmm. Only if the backend was validating that we always received a full valid 5-value cron string (with optional timezone). The only reason it would help is so that I know I can pass these strings directly from the backend into JS cron libraries for validation and display. Given that right now I have to first prepare them by using this One situation I don't want is a user being able to set a 5-value cron string via CLI or API that sets day of month and month values. While the backend may ignore them, the FE would accidentally produce a confusing human-readable string for them like this:
instead of
I plan on making the C/U form a set of widgets that under the hood produce a cron string. So imagine an input for time and another with the 7 days of the week that you select/deselect. Conceptually, the form will look like:
EDIT: Overall, my opinion is that the backend remains with the 3-value crontab as the cost is simply just this function we're looking at right now, which I think is acceptable. It doesn't seem like we fully bypass cost any which way unless the backend only supports 5-value cron strings and validates that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Gotcha. Opened #993 for this; I can make the necessary frontend changes if needed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the only changes that would be needed in a PR targeting #993 would be removing this function, which is ezpz 🎉 |
||
const prepared = stripTimezone(schedule).trim() | ||
|
||
const parts = prepared.split(" ") | ||
|
||
while (parts.length < 5) { | ||
// insert '*' in the second to last position | ||
// ie [a, b, c] --> [a, b, *, c] | ||
parts.splice(parts.length - 1, 0, "*") | ||
} | ||
|
||
return parts.join(" ") | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5458,6 +5458,11 @@ create-require@^1.1.0: | |
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" | ||
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== | ||
|
||
[email protected]: | ||
version "2.2.0" | ||
resolved "https://registry.yarnpkg.com/cronstrue/-/cronstrue-2.2.0.tgz#8e02b8ef0fa70a9eab9999f1f838df4bd378b471" | ||
integrity sha512-oM/ftAvCNIdygVGGfYp8gxrVc81mDSA2mff0kvu6+ehrZhfYPzGHG8DVcFdrRVizjHnzWoFIlgEq6KTM/9lPBw== | ||
|
||
cross-fetch@^3.0.4: | ||
version "3.1.5" | ||
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice