mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
Make form depend on switches
This commit is contained in:
@@ -17,6 +17,8 @@ import relativeTime from "dayjs/plugin/relativeTime"
|
||||
import timezone from "dayjs/plugin/timezone"
|
||||
import utc from "dayjs/plugin/utc"
|
||||
import { FormikTouched, useFormik } from "formik"
|
||||
import { AutoStart } from "pages/WorkspacesPage/schedule"
|
||||
import { AutoStop } from "pages/WorkspacesPage/ttl"
|
||||
import { FC } from "react"
|
||||
import * as Yup from "yup"
|
||||
import { getFormHelpersWithError } from "../../util/formUtils"
|
||||
@@ -57,7 +59,10 @@ export const Language = {
|
||||
|
||||
export interface WorkspaceScheduleFormProps {
|
||||
submitScheduleError?: Error | unknown
|
||||
initialValues?: WorkspaceScheduleFormValues
|
||||
autoStart: AutoStart
|
||||
toggleAutoStart: () => void
|
||||
autoStop: AutoStop
|
||||
toggleAutoStop: () => void
|
||||
isLoading: boolean
|
||||
onCancel: () => void
|
||||
onSubmit: (values: WorkspaceScheduleFormValues) => void
|
||||
@@ -162,54 +167,23 @@ export const validationSchema = Yup.object({
|
||||
.max(24 * 7 /* 7 days */),
|
||||
})
|
||||
|
||||
export const defaultWorkspaceScheduleTTL = 8
|
||||
|
||||
export const emptyWorkspaceSchedule = (
|
||||
ttl = 0,
|
||||
timezone = dayjs.tz.guess(),
|
||||
): WorkspaceScheduleFormValues => ({
|
||||
sunday: false,
|
||||
monday: false,
|
||||
tuesday: false,
|
||||
wednesday: false,
|
||||
thursday: false,
|
||||
friday: false,
|
||||
saturday: false,
|
||||
|
||||
startTime: "",
|
||||
timezone,
|
||||
ttl,
|
||||
})
|
||||
|
||||
export const defaultWorkspaceSchedule = (
|
||||
ttl = defaultWorkspaceScheduleTTL,
|
||||
timezone = dayjs.tz.guess(),
|
||||
): WorkspaceScheduleFormValues => ({
|
||||
sunday: false,
|
||||
monday: true,
|
||||
tuesday: true,
|
||||
wednesday: true,
|
||||
thursday: true,
|
||||
friday: true,
|
||||
saturday: false,
|
||||
|
||||
startTime: "09:30",
|
||||
timezone,
|
||||
ttl,
|
||||
})
|
||||
|
||||
export const WorkspaceScheduleForm: FC<WorkspaceScheduleFormProps> = ({
|
||||
submitScheduleError,
|
||||
initialValues = defaultWorkspaceSchedule(),
|
||||
autoStart,
|
||||
toggleAutoStart,
|
||||
autoStop,
|
||||
toggleAutoStop,
|
||||
isLoading,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
initialTouched,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
const initialValues = { ...autoStart.schedule, ttl: autoStop.ttl }
|
||||
|
||||
const form = useFormik<WorkspaceScheduleFormValues>({
|
||||
initialValues,
|
||||
enableReinitialize: true,
|
||||
onSubmit,
|
||||
validationSchema,
|
||||
initialTouched,
|
||||
@@ -236,12 +210,12 @@ export const WorkspaceScheduleForm: FC<WorkspaceScheduleFormProps> = ({
|
||||
{submitScheduleError && <ErrorSummary error={submitScheduleError} />}
|
||||
<Section title="Start">
|
||||
<FormControlLabel
|
||||
control={<Switch />}
|
||||
control={<Switch checked={autoStart.enabled} onChange={toggleAutoStart} />}
|
||||
label="Auto-start"
|
||||
/>
|
||||
<TextField
|
||||
{...formHelpers("startTime", Language.startTimeHelperText)}
|
||||
disabled={isLoading}
|
||||
disabled={isLoading || !autoStart.enabled}
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
}}
|
||||
@@ -251,7 +225,7 @@ export const WorkspaceScheduleForm: FC<WorkspaceScheduleFormProps> = ({
|
||||
|
||||
<TextField
|
||||
{...formHelpers("timezone")}
|
||||
disabled={isLoading}
|
||||
disabled={isLoading || !autoStart.enabled}
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
}}
|
||||
@@ -276,7 +250,7 @@ export const WorkspaceScheduleForm: FC<WorkspaceScheduleFormProps> = ({
|
||||
control={
|
||||
<Checkbox
|
||||
checked={checkbox.value}
|
||||
disabled={isLoading}
|
||||
disabled={isLoading || !autoStart.enabled}
|
||||
onChange={form.handleChange}
|
||||
name={checkbox.name}
|
||||
color="primary"
|
||||
@@ -296,12 +270,12 @@ export const WorkspaceScheduleForm: FC<WorkspaceScheduleFormProps> = ({
|
||||
|
||||
<Section title="Stop">
|
||||
<FormControlLabel
|
||||
control={<Switch />}
|
||||
control={<Switch checked={autoStop.enabled} onChange={toggleAutoStop} />}
|
||||
label="Auto-Stop"
|
||||
/>
|
||||
<TextField
|
||||
{...formHelpers("ttl", ttlShutdownAt(form.values.ttl), "ttl_ms")}
|
||||
disabled={isLoading}
|
||||
disabled={isLoading || !autoStop.enabled}
|
||||
inputProps={{ min: 0, step: 1 }}
|
||||
label={Language.ttlLabel}
|
||||
type="number"
|
||||
|
||||
@@ -1,30 +1,20 @@
|
||||
import { useMachine, useSelector } from "@xstate/react"
|
||||
import * as cronParser from "cron-parser"
|
||||
import dayjs from "dayjs"
|
||||
import timezone from "dayjs/plugin/timezone"
|
||||
import utc from "dayjs/plugin/utc"
|
||||
import React, { useContext, useEffect } from "react"
|
||||
import { defaultSchedule, emptySchedule, scheduleToAutoStart } from "pages/WorkspacesPage/schedule"
|
||||
import { ttlMsToAutoStop, emptyTTL, defaultTTL } from "pages/WorkspacesPage/ttl"
|
||||
import React, { useContext, useEffect, useState } from "react"
|
||||
import { useNavigate, useParams } from "react-router-dom"
|
||||
import * as TypesGen from "../../api/typesGenerated"
|
||||
import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary"
|
||||
import { FullScreenLoader } from "../../components/Loader/FullScreenLoader"
|
||||
import {
|
||||
emptyWorkspaceSchedule,
|
||||
WorkspaceScheduleForm,
|
||||
WorkspaceScheduleFormValues,
|
||||
} from "../../components/WorkspaceScheduleForm/WorkspaceScheduleForm"
|
||||
import { firstOrItem } from "../../util/array"
|
||||
import { extractTimezone, stripTimezone } from "../../util/schedule"
|
||||
import { selectUser } from "../../xServices/auth/authSelectors"
|
||||
import { XServiceContext } from "../../xServices/StateContext"
|
||||
import { workspaceSchedule } from "../../xServices/workspaceSchedule/workspaceScheduleXService"
|
||||
|
||||
// REMARK: timezone plugin depends on UTC
|
||||
//
|
||||
// SEE: https://day.js.org/docs/en/timezone/timezone
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
|
||||
const Language = {
|
||||
forbiddenError: "You don't have permissions to update the schedule for this workspace.",
|
||||
getWorkspaceError: "Failed to fetch workspace.",
|
||||
@@ -103,46 +93,6 @@ export const formValuesToTTLRequest = (
|
||||
}
|
||||
}
|
||||
|
||||
export const workspaceToInitialValues = (
|
||||
workspace: TypesGen.Workspace,
|
||||
defaultTimeZone = "",
|
||||
): WorkspaceScheduleFormValues => {
|
||||
const schedule = workspace.autostart_schedule
|
||||
const ttlHours = workspace.ttl_ms
|
||||
? Math.round(workspace.ttl_ms / (1000 * 60 * 60))
|
||||
: 0
|
||||
|
||||
if (!schedule) {
|
||||
return emptyWorkspaceSchedule(ttlHours, defaultTimeZone)
|
||||
}
|
||||
|
||||
const timezone = extractTimezone(schedule, defaultTimeZone)
|
||||
|
||||
const expression = cronParser.parseExpression(stripTimezone(schedule))
|
||||
|
||||
const HH = expression.fields.hour.join("").padStart(2, "0")
|
||||
const mm = expression.fields.minute.join("").padStart(2, "0")
|
||||
|
||||
const weeklyFlags = [false, false, false, false, false, false, false]
|
||||
|
||||
for (const day of expression.fields.dayOfWeek) {
|
||||
weeklyFlags[day % 7] = true
|
||||
}
|
||||
|
||||
return {
|
||||
sunday: weeklyFlags[0],
|
||||
monday: weeklyFlags[1],
|
||||
tuesday: weeklyFlags[2],
|
||||
wednesday: weeklyFlags[3],
|
||||
thursday: weeklyFlags[4],
|
||||
friday: weeklyFlags[5],
|
||||
saturday: weeklyFlags[6],
|
||||
startTime: `${HH}:${mm}`,
|
||||
timezone,
|
||||
ttl: ttlHours,
|
||||
}
|
||||
}
|
||||
|
||||
export const WorkspaceSchedulePage: React.FC = () => {
|
||||
const { username: usernameQueryParam, workspace: workspaceQueryParam } = useParams()
|
||||
const navigate = useNavigate()
|
||||
@@ -166,6 +116,63 @@ export const WorkspaceSchedulePage: React.FC = () => {
|
||||
username && workspaceName && scheduleSend({ type: "GET_WORKSPACE", username, workspaceName })
|
||||
}, [username, workspaceName, scheduleSend])
|
||||
|
||||
const getAutoStart = (workspace?: TypesGen.Workspace) => scheduleToAutoStart(workspace?.autostart_schedule)
|
||||
const getAutoStop = (workspace?: TypesGen.Workspace) => ttlMsToAutoStop(workspace?.ttl_ms)
|
||||
|
||||
const [autoStart, setAutoStart] = useState(getAutoStart(workspace))
|
||||
const [autoStop, setAutoStop] = useState(getAutoStop(workspace))
|
||||
|
||||
useEffect(() => {
|
||||
setAutoStart(getAutoStart(workspace))
|
||||
setAutoStop(getAutoStop(workspace))
|
||||
}, [workspace])
|
||||
|
||||
const onToggleAutoStart = () => {
|
||||
if (autoStart.enabled) {
|
||||
setAutoStart({
|
||||
enabled: false,
|
||||
schedule: emptySchedule
|
||||
})
|
||||
} else {
|
||||
if (workspace?.autostart_schedule) {
|
||||
// repopulate saved schedule
|
||||
setAutoStart({
|
||||
enabled: true,
|
||||
schedule: getAutoStart(workspace).schedule
|
||||
})
|
||||
} else {
|
||||
// populate with defaults
|
||||
setAutoStart({
|
||||
enabled: true,
|
||||
schedule: defaultSchedule()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onToggleAutoStop = () => {
|
||||
if (autoStop.enabled) {
|
||||
setAutoStop({
|
||||
enabled: false,
|
||||
ttl: emptyTTL
|
||||
})
|
||||
} else {
|
||||
if (workspace?.ttl_ms) {
|
||||
// repopulate saved ttl
|
||||
setAutoStop({
|
||||
enabled: true,
|
||||
ttl: getAutoStop(workspace).ttl
|
||||
})
|
||||
} else {
|
||||
// set default
|
||||
setAutoStop({
|
||||
enabled: true,
|
||||
ttl: defaultTTL
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!username || !workspaceName) {
|
||||
navigate("/workspaces")
|
||||
return null
|
||||
@@ -200,7 +207,10 @@ export const WorkspaceSchedulePage: React.FC = () => {
|
||||
return (
|
||||
<WorkspaceScheduleForm
|
||||
submitScheduleError={submitScheduleError}
|
||||
initialValues={workspaceToInitialValues(workspace, dayjs.tz.guess())}
|
||||
autoStart={autoStart}
|
||||
toggleAutoStart={onToggleAutoStart}
|
||||
autoStop={autoStop}
|
||||
toggleAutoStop={onToggleAutoStop}
|
||||
isLoading={scheduleState.tags.has("loading")}
|
||||
onCancel={() => {
|
||||
navigate(`/@${username}/${workspaceName}`)
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import * as cronParser from "cron-parser"
|
||||
import dayjs from "dayjs"
|
||||
import timezone from "dayjs/plugin/timezone"
|
||||
import utc from "dayjs/plugin/utc"
|
||||
import { extractTimezone, stripTimezone } from "../../util/schedule"
|
||||
|
||||
// REMARK: timezone plugin depends on UTC
|
||||
//
|
||||
// SEE: https://day.js.org/docs/en/timezone/timezone
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
|
||||
export interface AutoStartSchedule {
|
||||
sunday: boolean
|
||||
monday: boolean
|
||||
tuesday: boolean
|
||||
wednesday: boolean
|
||||
thursday: boolean
|
||||
friday: boolean
|
||||
saturday: boolean
|
||||
startTime: string
|
||||
timezone: string
|
||||
}
|
||||
|
||||
export interface AutoStart {
|
||||
enabled: boolean
|
||||
schedule: AutoStartSchedule
|
||||
}
|
||||
|
||||
export const emptySchedule = {
|
||||
sunday: false,
|
||||
monday: false,
|
||||
tuesday: false,
|
||||
wednesday: false,
|
||||
thursday: false,
|
||||
friday: false,
|
||||
saturday: false,
|
||||
|
||||
startTime: "",
|
||||
timezone: "",
|
||||
}
|
||||
|
||||
export const defaultSchedule = (
|
||||
): AutoStartSchedule => ({
|
||||
sunday: false,
|
||||
monday: true,
|
||||
tuesday: true,
|
||||
wednesday: true,
|
||||
thursday: true,
|
||||
friday: true,
|
||||
saturday: false,
|
||||
|
||||
startTime: "09:30",
|
||||
timezone: dayjs.tz.guess(),
|
||||
})
|
||||
|
||||
const transformSchedule = (schedule: string) => {
|
||||
const timezone = extractTimezone(schedule, dayjs.tz.guess())
|
||||
|
||||
const expression = cronParser.parseExpression(stripTimezone(schedule))
|
||||
|
||||
const HH = expression.fields.hour.join("").padStart(2, "0")
|
||||
const mm = expression.fields.minute.join("").padStart(2, "0")
|
||||
|
||||
const weeklyFlags = [false, false, false, false, false, false, false]
|
||||
|
||||
for (const day of expression.fields.dayOfWeek) {
|
||||
weeklyFlags[day % 7] = true
|
||||
}
|
||||
|
||||
return {
|
||||
sunday: weeklyFlags[0],
|
||||
monday: weeklyFlags[1],
|
||||
tuesday: weeklyFlags[2],
|
||||
wednesday: weeklyFlags[3],
|
||||
thursday: weeklyFlags[4],
|
||||
friday: weeklyFlags[5],
|
||||
saturday: weeklyFlags[6],
|
||||
startTime: `${HH}:${mm}`,
|
||||
timezone,
|
||||
}
|
||||
}
|
||||
|
||||
export const scheduleToAutoStart = (schedule?: string): AutoStart => {
|
||||
if (schedule) {
|
||||
return {
|
||||
enabled: true,
|
||||
schedule: transformSchedule(schedule)
|
||||
}
|
||||
} else {
|
||||
return { enabled: false, schedule: emptySchedule }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export interface AutoStop {
|
||||
enabled: boolean
|
||||
ttl: number
|
||||
}
|
||||
|
||||
export const emptyTTL = 0
|
||||
|
||||
export const defaultTTL = 8
|
||||
|
||||
const msToHours = (ms: number) => Math.round(ms / (1000 * 60 * 60))
|
||||
|
||||
export const ttlMsToAutoStop = (ttl_ms?: number): AutoStop => (
|
||||
ttl_ms ? { enabled: true, ttl: msToHours(ttl_ms) } : { enabled: false, ttl: 0 }
|
||||
)
|
||||
Reference in New Issue
Block a user