Skip to content

feat: edit workspace schedule page #1701

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

Merged
merged 29 commits into from
May 26, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
58ecefa
feat: edit workspace schedule page
greyscaled May 24, 2022
3f73a6c
fixup! feat: edit workspace schedule page
greyscaled May 24, 2022
30dff81
Merge branch 'main' into vapurrmaid/gh-1455/part-3/page
greyscaled May 24, 2022
ecc6792
remove promise
greyscaled May 24, 2022
c0f28c3
Merge origin/main
greyscaled May 24, 2022
d61a332
refactor to map + add loading/disabled
greyscaled May 24, 2022
f60f59b
time validation
greyscaled May 24, 2022
406d465
more tests
greyscaled May 24, 2022
a6dff9d
Update site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx
greyscaled May 24, 2022
7a14859
fix routing
greyscaled May 25, 2022
1158645
handle formErrors
greyscaled May 25, 2022
7a050db
finalize machine
greyscaled May 25, 2022
1166924
add timezone
greyscaled May 25, 2022
947b4a0
switch to TTL (hours)
greyscaled May 25, 2022
4764e5c
adjust ttl
greyscaled May 25, 2022
ebc4965
initialization
greyscaled May 26, 2022
747b52f
fixup! initialization
greyscaled May 26, 2022
854f781
fixup! initialization
greyscaled May 26, 2022
8bedc8a
Merge origin/main
greyscaled May 26, 2022
6afbb74
improve error message
greyscaled May 26, 2022
5bacfb1
Apply suggestions from code review
greyscaled May 26, 2022
5b3adc3
Update site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tes…
greyscaled May 26, 2022
81e7e05
fix ttl initialization
greyscaled May 26, 2022
531df3e
Update site/src/util/schedule.test.ts
greyscaled May 26, 2022
7ae590a
Fix typo
greyscaled May 26, 2022
262d9e3
import ReactNode directly
greyscaled May 26, 2022
5d22197
guess timezone
greyscaled May 26, 2022
eda8ad8
fix test
greyscaled May 26, 2022
f59f056
lint
greyscaled May 26, 2022
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
Prev Previous commit
Next Next commit
initialization
  • Loading branch information
greyscaled committed May 26, 2022
commit ebc49651cca65431d7bb4799f63140076eb2d95a
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as TypesGen from "../../api/typesGenerated"
import { WorkspaceScheduleFormValues } from "../../components/WorkspaceStats/WorkspaceScheduleForm"
import { formValuesToAutoStartRequest, formValuesToTTLRequest } from "./WorkspaceSchedulePage"
import * as Mocks from "../../testHelpers/entities"
import { formValuesToAutoStartRequest, formValuesToTTLRequest, workspaceToInitialValues } from "./WorkspaceSchedulePage"

const validValues: WorkspaceScheduleFormValues = {
sunday: false,
Expand Down Expand Up @@ -149,4 +150,73 @@ describe("WorkspaceSchedulePage", () => {
expect(formValuesToTTLRequest(values)).toEqual(request)
})
})

describe("workspaceToInitialValues", () => {
it.each<[TypesGen.Workspace, WorkspaceScheduleFormValues]>([
// Empty case
[
{
...Mocks.MockWorkspace,
autostart_schedule: "",
ttl: undefined,
},
{
sunday: false,
monday: false,
tuesday: false,
wednesday: false,
thursday: false,
friday: false,
saturday: false,
startTime: "",
timezone: "",
ttl: 0,
},
],

// Basic case: 9:30 1-5 UTC running for 2 hours
[
{
...Mocks.MockWorkspace,
autostart_schedule: "30 9 * * 1-5",
ttl: 7_200_000_000_000,
},
{
sunday: false,
monday: true,
tuesday: true,
wednesday: true,
thursday: true,
friday: true,
saturday: false,
startTime: "09:30",
timezone: "",
ttl: 2,
},
],

// Complex case: 4:20 1 3-4 6 Canada/Eastern for 8 hours
[
{
...Mocks.MockWorkspace,
autostart_schedule: "CRON_TZ=Canada/Eastern 20 16 * * 1,3-4,6",
ttl: 28_800_000_000_000,
},
{
sunday: false,
monday: true,
tuesday: false,
wednesday: true,
thursday: true,
friday: false,
saturday: true,
startTime: "16:20",
timezone: "Canada/Eastern",
ttl: 8,
},
],
])(`workspaceToInitialValues(%p) returns %p`, (workspace, formValues) => {
expect(workspaceToInitialValues(workspace)).toEqual(formValues)
})
})
})
46 changes: 46 additions & 0 deletions site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
WorkspaceScheduleFormValues,
} from "../../components/WorkspaceStats/WorkspaceScheduleForm"
import { firstOrItem } from "../../util/array"
import { dowToWeeklyFlag, extractTimezone, stripTimezone } from "../../util/schedule"
import { workspaceSchedule } from "../../xServices/workspaceSchedule/workspaceScheduleXService"

export const formValuesToAutoStartRequest = (
Expand Down Expand Up @@ -81,6 +82,51 @@ export const formValuesToTTLRequest = (values: WorkspaceScheduleFormValues): Typ
}
}

export const workspaceToInitialValues = (workspace: TypesGen.Workspace): WorkspaceScheduleFormValues => {
const schedule = workspace.autostart_schedule

if (!schedule) {
return {
sunday: false,
monday: false,
tuesday: false,
wednesday: false,
thursday: false,
friday: false,
saturday: false,
startTime: "",
timezone: "",
ttl: 0,
}
}

const timezone = extractTimezone(schedule, "")
const cronString = stripTimezone(schedule)

// parts has the following format: "mm HH * * dow"
const parts = cronString.split(" ")

// -> we skip month and day-of-month
const mm = parts[0]
const HH = parts[1]
const dow = parts[4]

const weeklyFlags = dowToWeeklyFlag(dow)

return {
sunday: weeklyFlags[0],
monday: weeklyFlags[1],
tuesday: weeklyFlags[2],
wednesday: weeklyFlags[3],
thursday: weeklyFlags[4],
friday: weeklyFlags[5],
saturday: weeklyFlags[6],
startTime: `${HH.padStart(2, "0")}:${mm.padStart(2, "0")}`,
timezone,
ttl: workspace.ttl ? workspace.ttl / (1_000_000 * 1000 * 60 * 60) : 0,
}
}

export const WorkspaceSchedulePage: React.FC = () => {
const navigate = useNavigate()
const { workspace: workspaceQueryParam } = useParams()
Expand Down
23 changes: 22 additions & 1 deletion site/src/util/schedule.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { extractTimezone, stripTimezone } from "./schedule"
import { dowToWeeklyFlag, extractTimezone, stripTimezone, WeeklyFlag } from "./schedule"

describe("util/schedule", () => {
describe("stripTimezone", () => {
Expand All @@ -20,4 +20,25 @@ describe("util/schedule", () => {
expect(extractTimezone(input)).toBe(expected)
})
})

describe("dowToWeeklyFlag", () => {
it.each<[string, WeeklyFlag]>([
// All days
["*", [true, true, true, true, true, true, true]],
["1-7", [true, true, true, true, true, true, true]],

// Single number modulo 7
["3", [false, false, false, true, false, false, false]],
["0", [true, false, false, false, false, false, false]],
["7", [true, false, false, false, false, false, false]],
["8", [false, true, false, false, false, false, false]],

// Comma-separated Numbers, Ranges and Mixes
["1,3,5", [false, true, false, true, false, true, false]],
["1-2,4-5", [false, true, true, false, true, true, false]],
["1,3-4,6", [false, true, false, true, true, false, true]],
])(`dowToWeeklyFlag(%p) returns %p`, (dow, weeklyFlag) => {
expect(dowToWeeklyFlag(dow)).toEqual(weeklyFlag)
})
})
})
75 changes: 72 additions & 3 deletions site/src/util/schedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,83 @@ export const stripTimezone = (raw: string): string => {

/**
* extractTimezone returns a leading timezone from a schedule string if one is
* specified; otherwise DEFAULT_TIMEZONE
* specified; otherwise the specified defaultTZ
*/
export const extractTimezone = (raw: string): string => {
export const extractTimezone = (raw: string, defaultTZ = DEFAULT_TIMEZONE): 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
return defaultTZ
}
}

/**
* WeeklyFlag is an array represnting which days of the week are set or flagged
*
* @remarks
*
* A WeeklyFlag has an array size of 7 and should never have its size modified.
* The 0th index is Sunday
* The 6th index is Saturday
*/
export type WeeklyFlag = [boolean, boolean, boolean, boolean, boolean, boolean, boolean]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Review:

This type is very helpful and needed because we get some static analysis on the size of the arrays. The semantic meaning is like a bit mask where in the array, each index corresponds to a day and whether that day is "set" (true) or unset "false".

So in other words, it goes from Sunday -> Saturday.

This sort of thing is easier to understand than bit masks, and maps very cleanly to setting values for the checkbox group.


/**
* dowToWeeklyFlag converts a dow cron string to a WeeklyFlag array.
*
* @example
*
* dowToWeeklyFlag("1") // [false, true, false, false, false, false, false]
* dowToWeeklyFlag("1-5") // [false, true, true, true, true, true, false]
* dowToWeeklyFlag("1,3-4,6") // [false, true, false, true, true, false, true]
*/
export const dowToWeeklyFlag = (dow: string): WeeklyFlag => {
if (dow === "*") {
return [true, true, true, true, true, true, true]
}

const results: WeeklyFlag = [false, false, false, false, false, false, false]

const commaSeparatedRangeOrNum = dow.split(",")

for (const rangeOrNum of commaSeparatedRangeOrNum) {
const flags = processRangeOrNum(rangeOrNum)

flags.forEach((value, idx) => {
if (value) {
results[idx] = true
}
})
}

return results
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Review:

Before reading this function, I'd read https://github.com/coder/coder/pull/1701/files#r882361592

This builds upon processRangeOrNum by calling it in a loop after splitting the dow cron string by comma.


/**
* processRangeOrNum is a helper for dowToWeeklyFlag. It processes a range or
* number (modulo 7) into a Weeklyflag boolean array.
*
* @example
*
* processRangeOrNum("1") // [false, true, false, false, false, false, false]
* processRangeOrNum("1-5") // [false, true, true, true, true, true, false]
*/
const processRangeOrNum = (rangeOrNum: string): WeeklyFlag => {
const result: WeeklyFlag = [false, false, false, false, false, false, false]

const isRange = /^[0-9]-[0-9]$/.test(rangeOrNum)

if (isRange) {
const [first, last] = rangeOrNum.split("-")

for (let i = Number(first); i <= Number(last); i++) {
result[i % 7] = true
}
} else {
result[Number(rangeOrNum) % 7] = true
}

return result
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Review:

This is a "private" method (only meant to be used in dowToWeeklyFlag and thus is not exported.

I'd review this before I'd review dowToWeeklyFlag.

The algorithm does the following:

  • takes a number (ex: 1)
  • or number range (ex: 1-5)

and returns the associated WeeklyFlag array (see: https://github.com/coder/coder/pull/1701/files#r882360151).

I have a method that focuses just on number or range, because in the parent function dowToWeeklyFlag, you can have comma-separated values like this:

1,3,5
1-2,4-5

So this function is called in a loop on each comma-separated group.