mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
refactor(site): Refactor workspace schedule page (#7069)
This commit is contained in:
+12
-3
@@ -17,6 +17,7 @@ import { RequireAuth } from "./components/RequireAuth/RequireAuth"
|
||||
import { SettingsLayout } from "./components/SettingsLayout/SettingsLayout"
|
||||
import { DeploySettingsLayout } from "components/DeploySettingsLayout/DeploySettingsLayout"
|
||||
import { TemplateSettingsLayout } from "pages/TemplateSettingsPage/TemplateSettingsLayout"
|
||||
import { WorkspaceSettingsLayout } from "pages/WorkspaceSettingsPage/WorkspaceSettingsLayout"
|
||||
|
||||
// Lazy load pages
|
||||
// - Pages that are secondary, not in the main navigation or not usually accessed
|
||||
@@ -45,7 +46,10 @@ const WorkspaceBuildPage = lazy(
|
||||
)
|
||||
const WorkspacePage = lazy(() => import("./pages/WorkspacePage/WorkspacePage"))
|
||||
const WorkspaceSchedulePage = lazy(
|
||||
() => import("./pages/WorkspaceSchedulePage/WorkspaceSchedulePage"),
|
||||
() =>
|
||||
import(
|
||||
"./pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage"
|
||||
),
|
||||
)
|
||||
const TerminalPage = lazy(() => import("./pages/TerminalPage/TerminalPage"))
|
||||
const TemplatePermissionsPage = lazy(
|
||||
@@ -260,12 +264,17 @@ export const AppRouter: FC = () => {
|
||||
<Route path="/@:username">
|
||||
<Route path=":workspace">
|
||||
<Route index element={<WorkspacePage />} />
|
||||
<Route path="schedule" element={<WorkspaceSchedulePage />} />
|
||||
<Route
|
||||
path="builds/:buildNumber"
|
||||
element={<WorkspaceBuildPage />}
|
||||
/>
|
||||
<Route path="settings" element={<WorkspaceSettingsPage />} />
|
||||
<Route path="settings" element={<WorkspaceSettingsLayout />}>
|
||||
<Route index element={<WorkspaceSettingsPage />} />
|
||||
<Route
|
||||
path="schedule"
|
||||
element={<WorkspaceSchedulePage />}
|
||||
/>
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
@@ -74,7 +74,7 @@ export const WorkspaceSchedule: FC<
|
||||
<Link
|
||||
className={styles.scheduleAction}
|
||||
component={RouterLink}
|
||||
to={`/@${workspace.owner_name}/${workspace.name}/schedule`}
|
||||
to={`/@${workspace.owner_name}/${workspace.name}/settings/schedule`}
|
||||
>
|
||||
{Language.editScheduleLink}
|
||||
</Link>
|
||||
|
||||
@@ -6,8 +6,8 @@ import utc from "dayjs/plugin/utc"
|
||||
import {
|
||||
defaultSchedule,
|
||||
emptySchedule,
|
||||
} from "pages/WorkspaceSchedulePage/schedule"
|
||||
import { emptyTTL } from "pages/WorkspaceSchedulePage/ttl"
|
||||
} from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule"
|
||||
import { emptyTTL } from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/ttl"
|
||||
import { makeMockApiError } from "testHelpers/entities"
|
||||
import {
|
||||
WorkspaceScheduleForm,
|
||||
|
||||
@@ -8,8 +8,13 @@ import MenuItem from "@material-ui/core/MenuItem"
|
||||
import makeStyles from "@material-ui/core/styles/makeStyles"
|
||||
import Switch from "@material-ui/core/Switch"
|
||||
import TextField from "@material-ui/core/TextField"
|
||||
import { AlertBanner } from "components/AlertBanner/AlertBanner"
|
||||
import { Section } from "components/Section/Section"
|
||||
import {
|
||||
HorizontalForm,
|
||||
FormFooter,
|
||||
FormSection,
|
||||
FormFields,
|
||||
} from "components/Form/Form"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import dayjs from "dayjs"
|
||||
import advancedFormat from "dayjs/plugin/advancedFormat"
|
||||
import duration from "dayjs/plugin/duration"
|
||||
@@ -20,13 +25,10 @@ import { FormikTouched, useFormik } from "formik"
|
||||
import {
|
||||
defaultSchedule,
|
||||
emptySchedule,
|
||||
} from "pages/WorkspaceSchedulePage/schedule"
|
||||
} from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule"
|
||||
import { ChangeEvent, FC } from "react"
|
||||
import * as Yup from "yup"
|
||||
import { getFormHelpers } from "../../util/formUtils"
|
||||
import { FormFooter } from "../FormFooter/FormFooter"
|
||||
import { FullPageForm } from "../FullPageForm/FullPageForm"
|
||||
import { Stack } from "../Stack/Stack"
|
||||
import { zones } from "./zones"
|
||||
|
||||
// REMARK: some plugins depend on utc, so it's listed first. Otherwise they're
|
||||
@@ -48,16 +50,14 @@ export const Language = {
|
||||
errorTtlMax:
|
||||
"Please enter a limit that is less than or equal to 168 hours (7 days).",
|
||||
daysOfWeekLabel: "Days of Week",
|
||||
daySundayLabel: "Sunday",
|
||||
dayMondayLabel: "Monday",
|
||||
dayTuesdayLabel: "Tuesday",
|
||||
dayWednesdayLabel: "Wednesday",
|
||||
dayThursdayLabel: "Thursday",
|
||||
dayFridayLabel: "Friday",
|
||||
daySaturdayLabel: "Saturday",
|
||||
daySundayLabel: "Sun",
|
||||
dayMondayLabel: "Mon",
|
||||
dayTuesdayLabel: "Tue",
|
||||
dayWednesdayLabel: "Wed",
|
||||
dayThursdayLabel: "Thu",
|
||||
dayFridayLabel: "Fri",
|
||||
daySaturdayLabel: "Sat",
|
||||
startTimeLabel: "Start time",
|
||||
startTimeHelperText: "Your workspace will automatically start at this time.",
|
||||
noStartTimeHelperText: "Your workspace will not automatically start.",
|
||||
timezoneLabel: "Timezone",
|
||||
ttlLabel: "Time until shutdown (hours)",
|
||||
ttlCausesShutdownHelperText: "Your workspace will shut down",
|
||||
@@ -67,13 +67,13 @@ export const Language = {
|
||||
"Your workspace will not automatically shut down.",
|
||||
formTitle: "Workspace schedule",
|
||||
startSection: "Start",
|
||||
startSwitch: "Autostart",
|
||||
startSwitch: "Enable Autostart",
|
||||
stopSection: "Stop",
|
||||
stopSwitch: "Autostop",
|
||||
stopSwitch: "Enable Autostop",
|
||||
}
|
||||
|
||||
export interface WorkspaceScheduleFormProps {
|
||||
submitScheduleError?: Error | unknown
|
||||
submitScheduleError?: unknown
|
||||
initialValues: WorkspaceScheduleFormValues
|
||||
isLoading: boolean
|
||||
onCancel: () => void
|
||||
@@ -280,31 +280,26 @@ export const WorkspaceScheduleForm: FC<
|
||||
}
|
||||
|
||||
return (
|
||||
<FullPageForm title={Language.formTitle}>
|
||||
<form onSubmit={form.handleSubmit} className={styles.form}>
|
||||
<Stack>
|
||||
{Boolean(submitScheduleError) && (
|
||||
<AlertBanner severity="error" error={submitScheduleError} />
|
||||
)}
|
||||
<Section title={Language.startSection}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
name="autostartEnabled"
|
||||
checked={form.values.autostartEnabled}
|
||||
onChange={handleToggleAutostart}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label={Language.startSwitch}
|
||||
/>
|
||||
<HorizontalForm onSubmit={form.handleSubmit}>
|
||||
<FormSection
|
||||
title="Autostart"
|
||||
description="Select the time and days of week on which you want the workspace starting automatically."
|
||||
>
|
||||
<FormFields>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
name="autostartEnabled"
|
||||
checked={form.values.autostartEnabled}
|
||||
onChange={handleToggleAutostart}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label={Language.startSwitch}
|
||||
/>
|
||||
<Stack direction="row">
|
||||
<TextField
|
||||
{...formHelpers(
|
||||
"startTime",
|
||||
form.values.autostartEnabled
|
||||
? Language.startTimeHelperText
|
||||
: Language.noStartTimeHelperText,
|
||||
)}
|
||||
{...formHelpers("startTime")}
|
||||
disabled={isLoading || !form.values.autostartEnabled}
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
@@ -313,7 +308,6 @@ export const WorkspaceScheduleForm: FC<
|
||||
type="time"
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<TextField
|
||||
{...formHelpers("timezone")}
|
||||
disabled={isLoading || !form.values.autostartEnabled}
|
||||
@@ -330,67 +324,68 @@ export const WorkspaceScheduleForm: FC<
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
</Stack>
|
||||
|
||||
<FormControl
|
||||
component="fieldset"
|
||||
error={Boolean(form.errors.monday)}
|
||||
>
|
||||
<FormLabel className={styles.daysOfWeekLabel} component="legend">
|
||||
{Language.daysOfWeekLabel}
|
||||
</FormLabel>
|
||||
<FormControl component="fieldset" error={Boolean(form.errors.monday)}>
|
||||
<FormLabel className={styles.daysOfWeekLabel} component="legend">
|
||||
{Language.daysOfWeekLabel}
|
||||
</FormLabel>
|
||||
|
||||
<FormGroup>
|
||||
{checkboxes.map((checkbox) => (
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={checkbox.value}
|
||||
disabled={isLoading || !form.values.autostartEnabled}
|
||||
onChange={form.handleChange}
|
||||
name={checkbox.name}
|
||||
color="primary"
|
||||
size="small"
|
||||
disableRipple
|
||||
/>
|
||||
}
|
||||
key={checkbox.name}
|
||||
label={checkbox.label}
|
||||
/>
|
||||
))}
|
||||
</FormGroup>
|
||||
|
||||
{form.errors.monday && (
|
||||
<FormHelperText>{Language.errorNoDayOfWeek}</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
</Section>
|
||||
|
||||
<Section title={Language.stopSection}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
name="autostopEnabled"
|
||||
checked={form.values.autostopEnabled}
|
||||
onChange={handleToggleAutostop}
|
||||
color="primary"
|
||||
<FormGroup className={styles.daysOfWeekOptions}>
|
||||
{checkboxes.map((checkbox) => (
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={checkbox.value}
|
||||
disabled={isLoading || !form.values.autostartEnabled}
|
||||
onChange={form.handleChange}
|
||||
name={checkbox.name}
|
||||
color="primary"
|
||||
size="small"
|
||||
disableRipple
|
||||
/>
|
||||
}
|
||||
key={checkbox.name}
|
||||
label={checkbox.label}
|
||||
/>
|
||||
}
|
||||
label={Language.stopSwitch}
|
||||
/>
|
||||
<TextField
|
||||
{...formHelpers("ttl", ttlShutdownAt(form.values.ttl), "ttl_ms")}
|
||||
disabled={isLoading || !form.values.autostopEnabled}
|
||||
inputProps={{ min: 0, step: 1 }}
|
||||
label={Language.ttlLabel}
|
||||
type="number"
|
||||
fullWidth
|
||||
/>
|
||||
</Section>
|
||||
))}
|
||||
</FormGroup>
|
||||
|
||||
<FormFooter onCancel={onCancel} isLoading={isLoading} />
|
||||
</Stack>
|
||||
</form>
|
||||
</FullPageForm>
|
||||
{form.errors.monday && (
|
||||
<FormHelperText>{Language.errorNoDayOfWeek}</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
|
||||
<FormSection
|
||||
title="Autostop"
|
||||
description="Set how many hours should elapse after a workspace is started before it automatically shuts down. If workspace connection activity is detected, the autostop timer will be bumped up one hour."
|
||||
>
|
||||
<FormFields>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
name="autostopEnabled"
|
||||
checked={form.values.autostopEnabled}
|
||||
onChange={handleToggleAutostop}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label={Language.stopSwitch}
|
||||
/>
|
||||
<TextField
|
||||
{...formHelpers("ttl", ttlShutdownAt(form.values.ttl), "ttl_ms")}
|
||||
disabled={isLoading || !form.values.autostopEnabled}
|
||||
inputProps={{ min: 0, step: 1 }}
|
||||
label={Language.ttlLabel}
|
||||
type="number"
|
||||
fullWidth
|
||||
/>
|
||||
</FormFields>
|
||||
</FormSection>
|
||||
<FormFooter onCancel={onCancel} isLoading={isLoading} />
|
||||
</HorizontalForm>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -405,13 +400,14 @@ export const ttlShutdownAt = (formTTL: number): string => {
|
||||
}
|
||||
}
|
||||
|
||||
const useStyles = makeStyles({
|
||||
form: {
|
||||
"& input": {
|
||||
colorScheme: "dark",
|
||||
},
|
||||
},
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
daysOfWeekLabel: {
|
||||
fontSize: 12,
|
||||
},
|
||||
})
|
||||
daysOfWeekOptions: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
paddingTop: theme.spacing(0.5),
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import { useMachine } from "@xstate/react"
|
||||
import { AlertBanner } from "components/AlertBanner/AlertBanner"
|
||||
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"
|
||||
import { Loader } from "components/Loader/Loader"
|
||||
import { Margins } from "components/Margins/Margins"
|
||||
import dayjs from "dayjs"
|
||||
import { scheduleToAutostart } from "pages/WorkspaceSchedulePage/schedule"
|
||||
import { ttlMsToAutostop } from "pages/WorkspaceSchedulePage/ttl"
|
||||
import { useEffect, FC } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Navigate, useNavigate, useParams } from "react-router-dom"
|
||||
import { scheduleChanged } from "util/schedule"
|
||||
import * as TypesGen from "../../api/typesGenerated"
|
||||
import { WorkspaceScheduleForm } from "../../components/WorkspaceScheduleForm/WorkspaceScheduleForm"
|
||||
import { firstOrItem } from "../../util/array"
|
||||
import { workspaceSchedule } from "../../xServices/workspaceSchedule/workspaceScheduleXService"
|
||||
import {
|
||||
formValuesToAutostartRequest,
|
||||
formValuesToTTLRequest,
|
||||
} from "./formToRequest"
|
||||
|
||||
const getAutostart = (workspace?: TypesGen.Workspace) =>
|
||||
scheduleToAutostart(workspace?.autostart_schedule)
|
||||
const getAutostop = (workspace?: TypesGen.Workspace) =>
|
||||
ttlMsToAutostop(workspace?.ttl_ms)
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
topMargin: {
|
||||
marginTop: `${theme.spacing(3)}px`,
|
||||
},
|
||||
}))
|
||||
|
||||
export const WorkspaceSchedulePage: FC = () => {
|
||||
const { t } = useTranslation("workspaceSchedulePage")
|
||||
const styles = useStyles()
|
||||
const { username: usernameQueryParam, workspace: workspaceQueryParam } =
|
||||
useParams()
|
||||
const navigate = useNavigate()
|
||||
const username = firstOrItem(usernameQueryParam, null)
|
||||
const workspaceName = firstOrItem(workspaceQueryParam, null)
|
||||
const [scheduleState, scheduleSend] = useMachine(workspaceSchedule)
|
||||
const {
|
||||
checkPermissionsError,
|
||||
submitScheduleError,
|
||||
getWorkspaceError,
|
||||
getTemplateError,
|
||||
permissions,
|
||||
workspace,
|
||||
template,
|
||||
} = scheduleState.context
|
||||
|
||||
// Get workspace on mount and whenever the args for getting a workspace change.
|
||||
// scheduleSend should not change.
|
||||
useEffect(() => {
|
||||
username &&
|
||||
workspaceName &&
|
||||
scheduleSend({ type: "GET_WORKSPACE", username, workspaceName })
|
||||
}, [username, workspaceName, scheduleSend])
|
||||
|
||||
if (!username || !workspaceName) {
|
||||
return <Navigate to="/workspaces" />
|
||||
}
|
||||
|
||||
if (scheduleState.hasTag("loading") || !template) {
|
||||
return <Loader />
|
||||
}
|
||||
|
||||
if (scheduleState.matches("error")) {
|
||||
return (
|
||||
<Margins>
|
||||
<div className={styles.topMargin}>
|
||||
<AlertBanner
|
||||
severity="error"
|
||||
error={
|
||||
getWorkspaceError || checkPermissionsError || getTemplateError
|
||||
}
|
||||
retry={() =>
|
||||
scheduleSend({ type: "GET_WORKSPACE", username, workspaceName })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Margins>
|
||||
)
|
||||
}
|
||||
|
||||
if (!permissions?.updateWorkspace) {
|
||||
return (
|
||||
<Margins>
|
||||
<div className={styles.topMargin}>
|
||||
<AlertBanner severity="error" error={Error(t("forbiddenError"))} />
|
||||
</div>
|
||||
</Margins>
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
scheduleState.matches("presentForm") ||
|
||||
scheduleState.matches("submittingSchedule")
|
||||
) {
|
||||
return (
|
||||
<WorkspaceScheduleForm
|
||||
submitScheduleError={submitScheduleError}
|
||||
initialValues={{
|
||||
...getAutostart(workspace),
|
||||
...getAutostop(workspace),
|
||||
}}
|
||||
isLoading={scheduleState.tags.has("loading")}
|
||||
defaultTTL={dayjs.duration(template.default_ttl_ms, "ms").asHours()}
|
||||
onCancel={() => {
|
||||
navigate(`/@${username}/${workspaceName}`)
|
||||
}}
|
||||
onSubmit={(values) => {
|
||||
scheduleSend({
|
||||
type: "SUBMIT_SCHEDULE",
|
||||
autostart: formValuesToAutostartRequest(values),
|
||||
ttl: formValuesToTTLRequest(values),
|
||||
autostartChanged: scheduleChanged(getAutostart(workspace), values),
|
||||
autostopChanged: scheduleChanged(getAutostop(workspace), values),
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (scheduleState.matches("showingRestartDialog")) {
|
||||
return (
|
||||
<ConfirmDialog
|
||||
open
|
||||
title={t("dialogTitle")}
|
||||
description={t("dialogDescription")}
|
||||
confirmText={t("restart")}
|
||||
cancelText={t("applyLater")}
|
||||
hideCancel={false}
|
||||
onConfirm={() => {
|
||||
scheduleSend("RESTART_WORKSPACE")
|
||||
}}
|
||||
onClose={() => {
|
||||
scheduleSend("APPLY_LATER")
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (scheduleState.matches("done")) {
|
||||
return <Navigate to={`/@${username}/${workspaceName}`} />
|
||||
}
|
||||
|
||||
// Theoretically impossible - log and bail
|
||||
console.error("WorkspaceSchedulePage: unknown state :: ", scheduleState)
|
||||
return <Navigate to="/" />
|
||||
}
|
||||
|
||||
export default WorkspaceSchedulePage
|
||||
@@ -0,0 +1,138 @@
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import ScheduleIcon from "@material-ui/icons/TimerOutlined"
|
||||
import { Workspace } from "api/typesGenerated"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import { FC, ElementType, PropsWithChildren, ReactNode } from "react"
|
||||
import { Link, NavLink } from "react-router-dom"
|
||||
import { combineClasses } from "util/combineClasses"
|
||||
import GeneralIcon from "@material-ui/icons/SettingsOutlined"
|
||||
import { Avatar } from "components/Avatar/Avatar"
|
||||
|
||||
const SidebarNavItem: FC<
|
||||
PropsWithChildren<{ href: string; icon: ReactNode }>
|
||||
> = ({ children, href, icon }) => {
|
||||
const styles = useStyles()
|
||||
return (
|
||||
<NavLink
|
||||
end
|
||||
to={href}
|
||||
className={({ isActive }) =>
|
||||
combineClasses([
|
||||
styles.sidebarNavItem,
|
||||
isActive ? styles.sidebarNavItemActive : undefined,
|
||||
])
|
||||
}
|
||||
>
|
||||
<Stack alignItems="center" spacing={1.5} direction="row">
|
||||
{icon}
|
||||
{children}
|
||||
</Stack>
|
||||
</NavLink>
|
||||
)
|
||||
}
|
||||
|
||||
const SidebarNavItemIcon: React.FC<{ icon: ElementType }> = ({
|
||||
icon: Icon,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
return <Icon className={styles.sidebarNavItemIcon} />
|
||||
}
|
||||
|
||||
export const Sidebar: React.FC<{ username: string; workspace: Workspace }> = ({
|
||||
username,
|
||||
workspace,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<nav className={styles.sidebar}>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
className={styles.workspaceInfo}
|
||||
>
|
||||
<Avatar src={workspace.template_icon} variant="square" fitImage />
|
||||
<Stack spacing={0} className={styles.workspaceData}>
|
||||
<Link className={styles.name} to={`/@${username}/${workspace.name}`}>
|
||||
{workspace.name}
|
||||
</Link>
|
||||
<span className={styles.secondary}>
|
||||
{workspace.template_display_name ?? workspace.template_name}
|
||||
</span>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<SidebarNavItem href="" icon={<SidebarNavItemIcon icon={GeneralIcon} />}>
|
||||
General
|
||||
</SidebarNavItem>
|
||||
<SidebarNavItem
|
||||
href="schedule"
|
||||
icon={<SidebarNavItemIcon icon={ScheduleIcon} />}
|
||||
>
|
||||
Schedule
|
||||
</SidebarNavItem>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
sidebar: {
|
||||
width: 245,
|
||||
flexShrink: 0,
|
||||
},
|
||||
sidebarNavItem: {
|
||||
color: "inherit",
|
||||
display: "block",
|
||||
fontSize: 14,
|
||||
textDecoration: "none",
|
||||
padding: theme.spacing(1.5, 1.5, 1.5, 2),
|
||||
borderRadius: theme.shape.borderRadius / 2,
|
||||
transition: "background-color 0.15s ease-in-out",
|
||||
marginBottom: 1,
|
||||
position: "relative",
|
||||
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
},
|
||||
},
|
||||
sidebarNavItemActive: {
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
|
||||
"&:before": {
|
||||
content: '""',
|
||||
display: "block",
|
||||
width: 3,
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
backgroundColor: theme.palette.secondary.dark,
|
||||
borderTopLeftRadius: theme.shape.borderRadius,
|
||||
borderBottomLeftRadius: theme.shape.borderRadius,
|
||||
},
|
||||
},
|
||||
sidebarNavItemIcon: {
|
||||
width: theme.spacing(2),
|
||||
height: theme.spacing(2),
|
||||
},
|
||||
workspaceInfo: {
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
workspaceData: {
|
||||
overflow: "hidden",
|
||||
},
|
||||
name: {
|
||||
fontWeight: 600,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
color: theme.palette.text.primary,
|
||||
textDecoration: "none",
|
||||
},
|
||||
secondary: {
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: 12,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
},
|
||||
}))
|
||||
+11
-8
@@ -1,16 +1,19 @@
|
||||
import { renderWithAuth } from "testHelpers/renderHelpers"
|
||||
import { renderWithWorkspaceSettingsLayout } from "testHelpers/renderHelpers"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import { screen } from "@testing-library/react"
|
||||
import {
|
||||
formValuesToAutostartRequest,
|
||||
formValuesToTTLRequest,
|
||||
} from "pages/WorkspaceSchedulePage/formToRequest"
|
||||
} from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/formToRequest"
|
||||
import {
|
||||
Autostart,
|
||||
scheduleToAutostart,
|
||||
} from "pages/WorkspaceSchedulePage/schedule"
|
||||
import { Autostop, ttlMsToAutostop } from "pages/WorkspaceSchedulePage/ttl"
|
||||
import * as TypesGen from "../../api/typesGenerated"
|
||||
} from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule"
|
||||
import {
|
||||
Autostop,
|
||||
ttlMsToAutostop,
|
||||
} from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/ttl"
|
||||
import * as TypesGen from "../../../api/typesGenerated"
|
||||
import {
|
||||
WorkspaceScheduleFormValues,
|
||||
Language as FormLanguage,
|
||||
@@ -257,7 +260,7 @@ describe("WorkspaceSchedulePage", () => {
|
||||
|
||||
describe("autostop change dialog", () => {
|
||||
it("shows if autostop is changed", async () => {
|
||||
renderWithAuth(<WorkspaceSchedulePage />, {
|
||||
renderWithWorkspaceSettingsLayout(<WorkspaceSchedulePage />, {
|
||||
route: `/@${MockUser.username}/${MockWorkspace.name}/schedule`,
|
||||
path: "/@:username/:workspace/schedule",
|
||||
})
|
||||
@@ -276,7 +279,7 @@ describe("WorkspaceSchedulePage", () => {
|
||||
})
|
||||
|
||||
it("doesn't show if autostop is not changed", async () => {
|
||||
renderWithAuth(<WorkspaceSchedulePage />, {
|
||||
renderWithWorkspaceSettingsLayout(<WorkspaceSchedulePage />, {
|
||||
route: `/@${MockUser.username}/${MockWorkspace.name}/schedule`,
|
||||
path: "/@:username/:workspace/schedule",
|
||||
})
|
||||
@@ -309,7 +312,7 @@ describe("WorkspaceSchedulePage", () => {
|
||||
},
|
||||
),
|
||||
)
|
||||
renderWithAuth(<WorkspaceSchedulePage />, {
|
||||
renderWithWorkspaceSettingsLayout(<WorkspaceSchedulePage />, {
|
||||
route: `/@${MockUser.username}/${MockWorkspace.name}/schedule`,
|
||||
path: "/@:username/:workspace/schedule",
|
||||
})
|
||||
@@ -0,0 +1,136 @@
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import { useMachine } from "@xstate/react"
|
||||
import { AlertBanner } from "components/AlertBanner/AlertBanner"
|
||||
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"
|
||||
import { Loader } from "components/Loader/Loader"
|
||||
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"
|
||||
import dayjs from "dayjs"
|
||||
import { scheduleToAutostart } from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule"
|
||||
import { ttlMsToAutostop } from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/ttl"
|
||||
import { useWorkspaceSettingsContext } from "pages/WorkspaceSettingsPage/WorkspaceSettingsLayout"
|
||||
import { FC } from "react"
|
||||
import { Helmet } from "react-helmet-async"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Navigate, useNavigate, useParams } from "react-router-dom"
|
||||
import { pageTitle } from "util/page"
|
||||
import { scheduleChanged } from "util/schedule"
|
||||
import * as TypesGen from "../../../api/typesGenerated"
|
||||
import { WorkspaceScheduleForm } from "../../../components/WorkspaceScheduleForm/WorkspaceScheduleForm"
|
||||
import { firstOrItem } from "../../../util/array"
|
||||
import { workspaceSchedule } from "../../../xServices/workspaceSchedule/workspaceScheduleXService"
|
||||
import {
|
||||
formValuesToAutostartRequest,
|
||||
formValuesToTTLRequest,
|
||||
} from "./formToRequest"
|
||||
|
||||
const getAutostart = (workspace: TypesGen.Workspace) =>
|
||||
scheduleToAutostart(workspace.autostart_schedule)
|
||||
const getAutostop = (workspace: TypesGen.Workspace) =>
|
||||
ttlMsToAutostop(workspace.ttl_ms)
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
topMargin: {
|
||||
marginTop: `${theme.spacing(3)}px`,
|
||||
},
|
||||
pageHeader: {
|
||||
paddingTop: 0,
|
||||
},
|
||||
}))
|
||||
|
||||
export const WorkspaceSchedulePage: FC = () => {
|
||||
const { t } = useTranslation("workspaceSchedulePage")
|
||||
const styles = useStyles()
|
||||
const { username: usernameQueryParam, workspace: workspaceQueryParam } =
|
||||
useParams()
|
||||
const navigate = useNavigate()
|
||||
const username = firstOrItem(usernameQueryParam, null)
|
||||
const workspaceName = firstOrItem(workspaceQueryParam, null)
|
||||
const { workspace } = useWorkspaceSettingsContext()
|
||||
const [scheduleState, scheduleSend] = useMachine(workspaceSchedule, {
|
||||
context: { workspace },
|
||||
})
|
||||
const {
|
||||
checkPermissionsError,
|
||||
submitScheduleError,
|
||||
getTemplateError,
|
||||
permissions,
|
||||
template,
|
||||
} = scheduleState.context
|
||||
|
||||
if (!username || !workspaceName) {
|
||||
return <Navigate to="/workspaces" />
|
||||
}
|
||||
|
||||
if (scheduleState.matches("done")) {
|
||||
return <Navigate to={`/@${username}/${workspaceName}`} />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{pageTitle([workspaceName, "Schedule"])}</title>
|
||||
</Helmet>
|
||||
<PageHeader className={styles.pageHeader}>
|
||||
<PageHeaderTitle>Workspace Schedule</PageHeaderTitle>
|
||||
</PageHeader>
|
||||
{(scheduleState.hasTag("loading") || !template) && <Loader />}
|
||||
{scheduleState.matches("error") && (
|
||||
<AlertBanner
|
||||
severity="error"
|
||||
error={checkPermissionsError || getTemplateError}
|
||||
/>
|
||||
)}
|
||||
{permissions && !permissions.updateWorkspace && (
|
||||
<AlertBanner severity="error" error={Error(t("forbiddenError"))} />
|
||||
)}
|
||||
{template &&
|
||||
workspace &&
|
||||
(scheduleState.matches("presentForm") ||
|
||||
scheduleState.matches("submittingSchedule")) && (
|
||||
<WorkspaceScheduleForm
|
||||
submitScheduleError={submitScheduleError}
|
||||
initialValues={{
|
||||
...getAutostart(workspace),
|
||||
...getAutostop(workspace),
|
||||
}}
|
||||
isLoading={scheduleState.tags.has("loading")}
|
||||
defaultTTL={dayjs.duration(template.default_ttl_ms, "ms").asHours()}
|
||||
onCancel={() => {
|
||||
navigate(`/@${username}/${workspaceName}`)
|
||||
}}
|
||||
onSubmit={(values) => {
|
||||
scheduleSend({
|
||||
type: "SUBMIT_SCHEDULE",
|
||||
autostart: formValuesToAutostartRequest(values),
|
||||
ttl: formValuesToTTLRequest(values),
|
||||
autostartChanged: scheduleChanged(
|
||||
getAutostart(workspace),
|
||||
values,
|
||||
),
|
||||
autostopChanged: scheduleChanged(
|
||||
getAutostop(workspace),
|
||||
values,
|
||||
),
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<ConfirmDialog
|
||||
open={scheduleState.matches("showingRestartDialog")}
|
||||
title={t("dialogTitle")}
|
||||
description={t("dialogDescription")}
|
||||
confirmText={t("restart")}
|
||||
cancelText={t("applyLater")}
|
||||
hideCancel={false}
|
||||
onConfirm={() => {
|
||||
scheduleSend("RESTART_WORKSPACE")
|
||||
}}
|
||||
onClose={() => {
|
||||
scheduleSend("APPLY_LATER")
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkspaceSchedulePage
|
||||
+1
-1
@@ -2,7 +2,7 @@ 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"
|
||||
import { extractTimezone, stripTimezone } from "../../../util/schedule"
|
||||
|
||||
// REMARK: timezone plugin depends on UTC
|
||||
//
|
||||
@@ -0,0 +1,86 @@
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import { Sidebar } from "./Sidebar"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import { createContext, FC, Suspense, useContext } from "react"
|
||||
import { Helmet } from "react-helmet-async"
|
||||
import { pageTitle } from "../../util/page"
|
||||
import { Loader } from "components/Loader/Loader"
|
||||
import { Outlet, useParams } from "react-router-dom"
|
||||
import { Margins } from "components/Margins/Margins"
|
||||
import { getWorkspaceByOwnerAndName } from "api/api"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
|
||||
const fetchWorkspaceSettings = async (owner: string, name: string) => {
|
||||
const workspace = await getWorkspaceByOwnerAndName(owner, name)
|
||||
|
||||
return {
|
||||
workspace,
|
||||
}
|
||||
}
|
||||
|
||||
const useWorkspace = (owner: string, name: string) => {
|
||||
return useQuery({
|
||||
queryKey: ["workspace", name, "settings"],
|
||||
queryFn: () => fetchWorkspaceSettings(owner, name),
|
||||
})
|
||||
}
|
||||
|
||||
const WorkspaceSettingsContext = createContext<
|
||||
Awaited<ReturnType<typeof fetchWorkspaceSettings>> | undefined
|
||||
>(undefined)
|
||||
|
||||
export const useWorkspaceSettingsContext = () => {
|
||||
const context = useContext(WorkspaceSettingsContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"useWorkspaceSettingsContext must be used within a WorkspaceSettingsContext.Provider",
|
||||
)
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
export const WorkspaceSettingsLayout: FC = () => {
|
||||
const styles = useStyles()
|
||||
const { workspace: workspaceName, username } = useParams() as {
|
||||
workspace: string
|
||||
username: string
|
||||
}
|
||||
const { data: settings } = useWorkspace(username, workspaceName)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{pageTitle([workspaceName, "Settings"])}</title>
|
||||
</Helmet>
|
||||
|
||||
{settings ? (
|
||||
<WorkspaceSettingsContext.Provider value={settings}>
|
||||
<Margins>
|
||||
<Stack className={styles.wrapper} direction="row" spacing={10}>
|
||||
<Sidebar workspace={settings.workspace} username={username} />
|
||||
<Suspense fallback={<Loader />}>
|
||||
<main className={styles.content}>
|
||||
<Outlet />
|
||||
</main>
|
||||
</Suspense>
|
||||
</Stack>
|
||||
</Margins>
|
||||
</WorkspaceSettingsContext.Provider>
|
||||
) : (
|
||||
<Loader />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
wrapper: {
|
||||
padding: theme.spacing(6, 0),
|
||||
},
|
||||
|
||||
content: {
|
||||
width: "100%",
|
||||
},
|
||||
}))
|
||||
@@ -1,6 +1,6 @@
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import {
|
||||
renderWithAuth,
|
||||
renderWithWorkspaceSettingsLayout,
|
||||
waitForLoaderToBeRemoved,
|
||||
} from "testHelpers/renderHelpers"
|
||||
import WorkspaceSettingsPage from "./WorkspaceSettingsPage"
|
||||
@@ -41,7 +41,7 @@ test("Submit the workspace settings page successfully", async () => {
|
||||
.mockResolvedValue(MockWorkspaceBuild)
|
||||
// Setup event and rendering
|
||||
const user = userEvent.setup()
|
||||
renderWithAuth(<WorkspaceSettingsPage />, {
|
||||
renderWithWorkspaceSettingsLayout(<WorkspaceSettingsPage />, {
|
||||
route: "/@test-user/test-workspace/settings",
|
||||
path: "/@:username/:workspace/settings",
|
||||
// Need this because after submit the user is redirected
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next"
|
||||
import { useNavigate, useParams } from "react-router-dom"
|
||||
import { pageTitle } from "util/page"
|
||||
import { useUpdateWorkspaceSettings, useWorkspaceSettings } from "./data"
|
||||
import { useWorkspaceSettingsContext } from "./WorkspaceSettingsLayout"
|
||||
import { WorkspaceSettingsPageView } from "./WorkspaceSettingsPageView"
|
||||
|
||||
const WorkspaceSettingsPage = () => {
|
||||
@@ -13,13 +14,10 @@ const WorkspaceSettingsPage = () => {
|
||||
username: string
|
||||
workspace: string
|
||||
}
|
||||
const {
|
||||
data: settings,
|
||||
error,
|
||||
isLoading,
|
||||
} = useWorkspaceSettings(username, workspaceName)
|
||||
const { workspace } = useWorkspaceSettingsContext()
|
||||
const { data: settings, error, isLoading } = useWorkspaceSettings(workspace)
|
||||
const navigate = useNavigate()
|
||||
const updateSettings = useUpdateWorkspaceSettings(settings?.workspace.id, {
|
||||
const updateSettings = useUpdateWorkspaceSettings(workspace.id, {
|
||||
onSuccess: ({ name }) => {
|
||||
navigate(`/@${username}/${name}`)
|
||||
},
|
||||
@@ -30,7 +28,7 @@ const WorkspaceSettingsPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{pageTitle(t("title"))}</title>
|
||||
<title>{pageTitle([workspaceName, "Settings"])}</title>
|
||||
</Helmet>
|
||||
|
||||
<WorkspaceSettingsPageView
|
||||
@@ -39,7 +37,7 @@ const WorkspaceSettingsPage = () => {
|
||||
isLoading={isLoading}
|
||||
isSubmitting={updateSettings.isLoading}
|
||||
settings={settings}
|
||||
onCancel={() => navigate(-1)}
|
||||
onCancel={() => navigate(`/@${username}/${workspaceName}`)}
|
||||
onSubmit={updateSettings.mutate}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import { AlertBanner } from "components/AlertBanner/AlertBanner"
|
||||
import { FullPageHorizontalForm } from "components/FullPageForm/FullPageHorizontalForm"
|
||||
import { Loader } from "components/Loader/Loader"
|
||||
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"
|
||||
import { FC } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { WorkspaceSettings, WorkspaceSettingsFormValue } from "./data"
|
||||
@@ -26,22 +27,31 @@ export const WorkspaceSettingsPageView: FC<WorkspaceSettingsPageViewProps> = ({
|
||||
loadingError,
|
||||
}) => {
|
||||
const { t } = useTranslation("workspaceSettingsPage")
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<FullPageHorizontalForm title={t("title")} onCancel={onCancel}>
|
||||
<>
|
||||
{loadingError && <AlertBanner error={loadingError} severity="error" />}
|
||||
{isLoading && <Loader />}
|
||||
{settings && (
|
||||
<WorkspaceSettingsForm
|
||||
error={formError}
|
||||
isSubmitting={isSubmitting}
|
||||
settings={settings}
|
||||
onCancel={onCancel}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</FullPageHorizontalForm>
|
||||
<>
|
||||
<PageHeader className={styles.pageHeader}>
|
||||
<PageHeaderTitle>{t("title")}</PageHeaderTitle>
|
||||
</PageHeader>
|
||||
|
||||
{loadingError && <AlertBanner error={loadingError} severity="error" />}
|
||||
{isLoading && <Loader />}
|
||||
{settings && (
|
||||
<WorkspaceSettingsForm
|
||||
error={formError}
|
||||
isSubmitting={isSubmitting}
|
||||
settings={settings}
|
||||
onCancel={onCancel}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
pageHeader: {
|
||||
paddingTop: 0,
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { useMutation, useQuery } from "@tanstack/react-query"
|
||||
import {
|
||||
getWorkspaceByOwnerAndName,
|
||||
getWorkspaceBuildParameters,
|
||||
getTemplateVersionRichParameters,
|
||||
patchWorkspace,
|
||||
postWorkspaceBuild,
|
||||
} from "api/api"
|
||||
import { WorkspaceBuildParameter } from "api/typesGenerated"
|
||||
import { Workspace, WorkspaceBuildParameter } from "api/typesGenerated"
|
||||
|
||||
const getWorkspaceSettings = async (owner: string, name: string) => {
|
||||
const workspace = await getWorkspaceByOwnerAndName(owner, name)
|
||||
const getWorkspaceSettings = async (workspace: Workspace) => {
|
||||
const latestBuild = workspace.latest_build
|
||||
const [templateVersionRichParameters, buildParameters] = await Promise.all([
|
||||
getTemplateVersionRichParameters(latestBuild.template_version_id),
|
||||
@@ -22,10 +20,10 @@ const getWorkspaceSettings = async (owner: string, name: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const useWorkspaceSettings = (owner: string, workspace: string) => {
|
||||
export const useWorkspaceSettings = (workspace: Workspace) => {
|
||||
return useQuery({
|
||||
queryKey: ["workspaceSettings", owner, workspace],
|
||||
queryFn: () => getWorkspaceSettings(owner, workspace),
|
||||
queryKey: ["workspaceSettings", workspace.id],
|
||||
queryFn: () => getWorkspaceSettings(workspace),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { DashboardLayout } from "components/Dashboard/DashboardLayout"
|
||||
import { createMemoryHistory } from "history"
|
||||
import { i18n } from "i18n"
|
||||
import { TemplateSettingsLayout } from "pages/TemplateSettingsPage/TemplateSettingsLayout"
|
||||
import { WorkspaceSettingsLayout } from "pages/WorkspaceSettingsPage/WorkspaceSettingsLayout"
|
||||
import { FC, ReactElement } from "react"
|
||||
import { I18nextProvider } from "react-i18next"
|
||||
import {
|
||||
@@ -131,6 +132,50 @@ export function renderWithTemplateSettingsLayout(
|
||||
}
|
||||
}
|
||||
|
||||
export function renderWithWorkspaceSettingsLayout(
|
||||
element: JSX.Element,
|
||||
{
|
||||
path = "/",
|
||||
route = "/",
|
||||
extraRoutes = [],
|
||||
nonAuthenticatedRoutes = [],
|
||||
}: RenderWithAuthOptions = {},
|
||||
) {
|
||||
const routes: RouteObject[] = [
|
||||
{
|
||||
element: <RequireAuth />,
|
||||
children: [
|
||||
{
|
||||
element: <DashboardLayout />,
|
||||
children: [
|
||||
{
|
||||
element: <WorkspaceSettingsLayout />,
|
||||
children: [{ path, element }, ...extraRoutes],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
...nonAuthenticatedRoutes,
|
||||
]
|
||||
|
||||
const router = createMemoryRouter(routes, { initialEntries: [route] })
|
||||
|
||||
const renderResult = wrappedRender(
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<AppProviders>
|
||||
<RouterProvider router={router} />
|
||||
</AppProviders>
|
||||
</I18nextProvider>,
|
||||
)
|
||||
|
||||
return {
|
||||
user: MockUser,
|
||||
router,
|
||||
...renderResult,
|
||||
}
|
||||
}
|
||||
|
||||
export const waitForLoaderToBeRemoved = (): Promise<void> =>
|
||||
// Sometimes, we have pages that are doing a lot of requests to get done, so the
|
||||
// default timeout of 1_000 is not enough. We should revisit this when we unify
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import dayjs from "dayjs"
|
||||
import duration from "dayjs/plugin/duration"
|
||||
import { emptySchedule } from "pages/WorkspaceSchedulePage/schedule"
|
||||
import { emptyTTL } from "pages/WorkspaceSchedulePage/ttl"
|
||||
import { emptySchedule } from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule"
|
||||
import { emptyTTL } from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/ttl"
|
||||
import { Workspace } from "../api/typesGenerated"
|
||||
import * as Mocks from "../testHelpers/entities"
|
||||
import {
|
||||
|
||||
@@ -10,8 +10,8 @@ import utc from "dayjs/plugin/utc"
|
||||
import { Workspace } from "../api/typesGenerated"
|
||||
import { isWorkspaceOn } from "./workspace"
|
||||
import { WorkspaceScheduleFormValues } from "components/WorkspaceScheduleForm/WorkspaceScheduleForm"
|
||||
import { Autostop } from "pages/WorkspaceSchedulePage/ttl"
|
||||
import { Autostart } from "pages/WorkspaceSchedulePage/schedule"
|
||||
import { Autostop } from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/ttl"
|
||||
import { Autostart } from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule"
|
||||
|
||||
// REMARK: some plugins depend on utc, so it's listed first. Otherwise they're
|
||||
// sorted alphabetically.
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface WorkspaceScheduleContext {
|
||||
* re-fetch the workspace to ensure we're up-to-date. As a result, this
|
||||
* machine is partially influenced by workspaceXService.
|
||||
*/
|
||||
workspace?: TypesGen.Workspace
|
||||
workspace: TypesGen.Workspace
|
||||
template?: TypesGen.Template
|
||||
getTemplateError?: Error | unknown
|
||||
permissions?: Permissions
|
||||
@@ -41,7 +41,6 @@ const permissionsToCheck = (workspace: TypesGen.Workspace) => ({
|
||||
})
|
||||
|
||||
export type WorkspaceScheduleEvent =
|
||||
| { type: "GET_WORKSPACE"; username: string; workspaceName: string }
|
||||
| {
|
||||
type: "SUBMIT_SCHEDULE"
|
||||
autostart: TypesGen.UpdateWorkspaceAutostartRequest
|
||||
@@ -63,38 +62,13 @@ export const workspaceSchedule =
|
||||
context: {} as WorkspaceScheduleContext,
|
||||
events: {} as WorkspaceScheduleEvent,
|
||||
services: {} as {
|
||||
getWorkspace: {
|
||||
data: TypesGen.Workspace
|
||||
}
|
||||
getTemplate: {
|
||||
data: TypesGen.Template
|
||||
}
|
||||
},
|
||||
},
|
||||
initial: "idle",
|
||||
on: {
|
||||
GET_WORKSPACE: "gettingWorkspace",
|
||||
},
|
||||
initial: "gettingPermissions",
|
||||
states: {
|
||||
idle: {
|
||||
tags: "loading",
|
||||
},
|
||||
gettingWorkspace: {
|
||||
entry: ["clearGetWorkspaceError", "clearContext"],
|
||||
invoke: {
|
||||
src: "getWorkspace",
|
||||
id: "getWorkspace",
|
||||
onDone: {
|
||||
target: "gettingPermissions",
|
||||
actions: ["assignWorkspace"],
|
||||
},
|
||||
onError: {
|
||||
target: "error",
|
||||
actions: ["assignGetWorkspaceError"],
|
||||
},
|
||||
},
|
||||
tags: "loading",
|
||||
},
|
||||
gettingPermissions: {
|
||||
entry: "clearGetPermissionsError",
|
||||
invoke: {
|
||||
@@ -167,9 +141,7 @@ export const workspaceSchedule =
|
||||
},
|
||||
},
|
||||
error: {
|
||||
on: {
|
||||
GET_WORKSPACE: "gettingWorkspace",
|
||||
},
|
||||
type: "final",
|
||||
},
|
||||
done: {
|
||||
type: "final",
|
||||
@@ -184,12 +156,6 @@ export const workspaceSchedule =
|
||||
assignSubmissionError: assign({
|
||||
submitScheduleError: (_, event) => event.data,
|
||||
}),
|
||||
assignWorkspace: assign({
|
||||
workspace: (_, event) => event.data,
|
||||
}),
|
||||
assignGetWorkspaceError: assign({
|
||||
getWorkspaceError: (_, event) => event.data,
|
||||
}),
|
||||
assignPermissions: assign({
|
||||
// Setting event.data as Permissions to be more stricted. So we know
|
||||
// what permissions we asked for.
|
||||
@@ -213,12 +179,7 @@ export const workspaceSchedule =
|
||||
clearGetPermissionsError: assign({
|
||||
checkPermissionsError: (_) => undefined,
|
||||
}),
|
||||
clearContext: () => {
|
||||
assign({ workspace: undefined, permissions: undefined })
|
||||
},
|
||||
clearGetWorkspaceError: (context) => {
|
||||
assign({ ...context, getWorkspaceError: undefined })
|
||||
},
|
||||
|
||||
// action instead of service because we fire and forget so that the
|
||||
// user can return to the workspace page to see the restart
|
||||
restartWorkspace: (context) => {
|
||||
@@ -232,12 +193,6 @@ export const workspaceSchedule =
|
||||
},
|
||||
|
||||
services: {
|
||||
getWorkspace: async (_, event) => {
|
||||
return await API.getWorkspaceByOwnerAndName(
|
||||
event.username,
|
||||
event.workspaceName,
|
||||
)
|
||||
},
|
||||
getTemplate: async (context) => {
|
||||
if (context.workspace) {
|
||||
return await API.getTemplate(context.workspace.template_id)
|
||||
|
||||
Reference in New Issue
Block a user