Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
greyscaled merged 2 commits intomainfromvapurrmaid/274/auto-on-off-components
Apr 14, 2022
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletionscoderd/autostart/schedule/schedule.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,6 +26,7 @@ var defaultParser = cron.NewParser(parserFormatWeekly)
// local_sched, _ := schedule.Weekly("59 23 *")
// fmt.Println(sched.Next(time.Now().Format(time.RFC3339)))
// // Output: 2022-04-04T23:59:00Z
//
// us_sched, _ := schedule.Weekly("CRON_TZ=US/Central 30 9 1-5")
// fmt.Println(sched.Next(time.Now()).Format(time.RFC3339))
// // Output: 2022-04-04T14:30:00Z
Expand Down
4 changes: 2 additions & 2 deletionscodersdk/workspaces.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -92,7 +92,7 @@ func (c *Client) WorkspaceBuildByName(ctx context.Context, workspace uuid.UUID,

// UpdateWorkspaceAutostartRequest is a request to update a workspace's autostart schedule.
type UpdateWorkspaceAutostartRequest struct {
Schedule string
Schedule string `json:"schedule"`
}

// UpdateWorkspaceAutostart sets the autostart schedule for workspace by id.
Expand All@@ -112,7 +112,7 @@ func (c *Client) UpdateWorkspaceAutostart(ctx context.Context, id uuid.UUID, req

// UpdateWorkspaceAutostopRequest is a request to update a workspace's autostop schedule.
type UpdateWorkspaceAutostopRequest struct {
Schedule string
Schedule string `json:"schedule"`
}

// UpdateWorkspaceAutostop sets the autostop schedule for workspace by id.
Expand Down
1 change: 1 addition & 0 deletionssite/package.json
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -32,6 +32,7 @@
"@xstate/inspect": "0.6.5",
"@xstate/react": "3.0.0",
"axios": "0.26.1",
"cronstrue": "2.2.0",
"formik": "2.2.9",
"history": "5.3.0",
"react": "17.0.2",
Expand Down
20 changes: 20 additions & 0 deletionssite/src/api/index.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -73,3 +73,23 @@ export const getBuildInfo = async (): Promise<Types.BuildInfoResponse> => {
const response = await axios.get("/api/v2/buildinfo")
return response.data
}

export const putWorkspaceAutostart = async (
workspaceID: string,
autostart: Types.WorkspaceAutostartRequest,
): Promise<void> => {
const payload = JSON.stringify(autostart)
await axios.put(`/api/v2/workspaces/${workspaceID}/autostart`, payload, {
headers: { ...CONTENT_TYPE_JSON },
})
}

export const putWorkspaceAutostop = async (
workspaceID: string,
autostop: Types.WorkspaceAutostopRequest,
): Promise<void> => {
const payload = JSON.stringify(autostop)
await axios.put(`/api/v2/workspaces/${workspaceID}/autostop`, payload, {
headers: { ...CONTENT_TYPE_JSON },
})
}
14 changes: 13 additions & 1 deletionsite/src/api/types.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -54,14 +54,18 @@ export interface CreateWorkspaceRequest {
template_id: string
}

// Must be kept in sync with backend Workspace struct
/**
* @remarks Keep in sync with codersdk/workspaces.go
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

nice

export interface Workspace {
id: string
created_at: string
updated_at: string
owner_id: string
template_id: string
name: string
autostart_schedule: string
autostop_schedule: string
}

export interface APIKeyResponse {
Expand All@@ -74,3 +78,11 @@ export interface UserAgent {
readonly ip_address: string
readonly os: string
}

export interface WorkspaceAutostartRequest {
schedule: string
}

export interface WorkspaceAutostopRequest {
schedule: string
}
2 changes: 2 additions & 0 deletionssite/src/components/Workspace/Workspace.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import React from "react"
import { Link } from "react-router-dom"
import * as Types from "../../api/types"
import * as Constants from "./constants"
import { WorkspaceSchedule } from "./WorkspaceSchedule"
import { WorkspaceSection } from "./WorkspaceSection"

export interface WorkspaceProps {
Expand All@@ -30,6 +31,7 @@ export const Workspace: React.FC<WorkspaceProps> = ({ organization, template, wo
<WorkspaceSection title="Applications">
<Placeholder />
</WorkspaceSection>
<WorkspaceSchedule autostart={workspace.autostart_schedule} autostop={workspace.autostop_schedule} />
<WorkspaceSection title="Dev URLs">
<Placeholder />
</WorkspaceSection>
Expand Down
17 changes: 17 additions & 0 deletionssite/src/components/Workspace/WorkspaceSchedule.stories.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff 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: "",
}
59 changes: 59 additions & 0 deletionssite/src/components/Workspace/WorkspaceSchedule.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff 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>
)
}
22 changes: 22 additions & 0 deletionssite/src/test_helpers/entities.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@ import {
UserAgent,
UserResponse,
Workspace,
WorkspaceAutostartRequest,
} from "../api/types"

export const MockSessionToken = { session_token: "my-session-token" }
Expand DownExpand Up@@ -46,13 +47,34 @@ export const MockTemplate: Template = {
active_version_id: "",
}

export const MockWorkspaceAutostartDisabled: WorkspaceAutostartRequest = {
schedule: "",
}

export const MockWorkspaceAutostartEnabled: WorkspaceAutostartRequest = {
// Runs at 9:30am Monday through Friday using Canada/Eastern
// (America/Toronto) time
schedule: "CRON_TZ=Canada/Eastern 30 9 1-5",
}

export const MockWorkspaceAutostopDisabled: WorkspaceAutostartRequest = {
schedule: "",
}

export const MockWorkspaceAutostopEnabled: WorkspaceAutostartRequest = {
// Runs at 9:30pm Monday through Friday using America/Toronto
schedule: "CRON_TZ=America/Toronto 30 21 1-5",
}

export const MockWorkspace: Workspace = {
id: "test-workspace",
name: "Test-Workspace",
created_at: "",
updated_at: "",
template_id: MockTemplate.id,
owner_id: MockUser.id,
autostart_schedule: MockWorkspaceAutostartEnabled.schedule,
autostop_schedule: MockWorkspaceAutostopEnabled.schedule,
}

export const MockUserAgent: UserAgent = {
Expand Down
6 changes: 6 additions & 0 deletionssite/src/test_helpers/handlers.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,4 +44,10 @@ export const handlers = [
rest.get("/api/v2/workspaces/:workspaceId", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockWorkspace))
}),
rest.put("/api/v2/workspaces/:workspaceId/autostart", async (req, res, ctx) => {
return res(ctx.status(200))
}),
rest.put("/api/v2/workspaces/:workspaceId/autostop", async (req, res, ctx) => {
return res(ctx.status(200))
}),
]
33 changes: 33 additions & 0 deletionssite/src/util/schedule.test.ts
View file
Open in desktop
Original file line numberDiff line numberDiff 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)
})
})
})
54 changes: 54 additions & 0 deletionssite/src/util/schedule.ts
View file
Open in desktop
Original file line numberDiff line numberDiff 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"
Copy link
Member

Choose a reason for hiding this comment

The 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 => {
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
ContributorAuthor

@greyscaledgreyscaledApr 13, 2022
edited
Loading

Choose a reason for hiding this comment

The 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 thisexpandScheduleCronString, which is a simple and well-tested function, I'm not overly concerned about it, but I do think a db migration for it that would have to expand stored values in the same way would be more complicated.

One situationI 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:

At 09:30 on day-of-month 1 and on every day-of-week from Monday through Friday in February.

instead of

At 09:30 on Monday through Friday.

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:

time (TIMEZONE)[ 9:30                  c ]days[ ] Sun [x] Mon [x] Tues [x] Weds [x] Thurs [x] Fri [ ] SatYour workspace will auto-[start/stop] at 09:30 on Monday through Friday.

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 thatday-of-month andmonth are*.

johnstcn reacted with thumbs up emoji
Copy link
Member

@johnstcnjohnstcnApr 13, 2022
edited
Loading

Choose a reason for hiding this comment

The 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.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The 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 🎉

johnstcn reacted with hooray emoji
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(" ")
}
5 changes: 5 additions & 0 deletionssite/yarn.lock
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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==

cronstrue@2.2.0:
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"
Expand Down

[8]ページ先頭

©2009-2025 Movatter.jp