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
Next Next commit
feat: edit workspace schedule page
  • Loading branch information
greyscaled committed May 24, 2022
commit 58ecefa121312f085bb60a11dc77c9b306c058b6
3 changes: 3 additions & 0 deletions site/src/components/WorkspaceStats/WorkspaceScheduleForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export const Language = {

export interface WorkspaceScheduleFormProps {
onCancel: () => void

// TODO(Grey): un-promisfy and adding isSubmitting prop
onSubmit: (values: WorkspaceScheduleFormValues) => Promise<void>
}

Expand Down Expand Up @@ -73,6 +75,7 @@ export const validationSchema = Yup.object({
friday: Yup.boolean(),
saturday: Yup.boolean(),

// TODO(Grey): Add validation that the string is "" or "HH:mm" (24 hours)
startTime: Yup.string(),
ttl: Yup.number().min(0).integer(),
})
Expand Down
88 changes: 88 additions & 0 deletions site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { useMachine } from "@xstate/react"
import React, { useEffect } 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 {
WorkspaceScheduleForm,
WorkspaceScheduleFormValues,
} from "../../components/WorkspaceStats/WorkspaceScheduleForm"
import { firstOrItem } from "../../util/array"
import { workspaceSchedule } from "../../xServices/workspaceSchedule/workspaceScheduleXService"

// TODO(Grey): Test before opening PR from draft
export const formValuesToAutoStartRequest = (
values: WorkspaceScheduleFormValues,
): TypesGen.UpdateWorkspaceAutostartRequest => {
if (!values.startTime) {
return {
schedule: "",
}
}

// TODO(Grey): Fill in
return {
schedule: "9 30 * * 1-5",
}
}

export const formValuesToTTLRequest = (values: WorkspaceScheduleFormValues): TypesGen.UpdateWorkspaceTTLRequest => {
if (!values.ttl) {
return {
ttl: 0, // TODO(Grey): Verify with Cian whether 0 or null is better to send
}
}

// TODO(Grey): Fill in
return {
ttl: 0,
}
}

// TODO(Grey): React testing library for this
export const WorkspaceSchedulePage: React.FC = () => {
const navigate = useNavigate()
const { workspace: workspaceQueryParam } = useParams()
const workspaceId = firstOrItem(workspaceQueryParam, null)

// TODO(Grey): Consume the formSubmissionErrors in WorkspaceScheduleForm
const [scheduleState, scheduleSend] = useMachine(workspaceSchedule)
const { getWorkspaceError, workspace } = scheduleState.context

/**
* Get workspace on mount and whenever workspaceId changes (scheduleSend
* should not change).
*/
useEffect(() => {
workspaceId && scheduleSend({ type: "GET_WORKSPACE", workspaceId })
}, [workspaceId, scheduleSend])

if (!workspaceId) {
navigate("/workspaces")
return null
} else if (scheduleState.matches("error")) {
return <ErrorSummary error={getWorkspaceError} retry={() => scheduleSend({ type: "GET_WORKSPACE", workspaceId })} />
} else if (!workspace) {
return <FullScreenLoader />
} else {
return (
<WorkspaceScheduleForm
onCancel={() => {
navigate(`/workspaces/${workspaceId}`)
}}
onSubmit={(values) => {
scheduleSend({
type: "SUBMIT_SCHEDULE",
autoStart: formValuesToAutoStartRequest(values),
ttl: formValuesToTTLRequest(values),
})

// TODO(Grey): Remove this after onSubmit is un-promisified
// TODO(Grey): navigation logic
return Promise.resolve()
}}
/>
)
}
}
146 changes: 146 additions & 0 deletions site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/**
* @fileoverview workspaceSchedule is an xstate machine backing a form to CRUD
* an individual workspace's schedule.
*/
Comment on lines +1 to +4
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:

Here's a GIF of me stepping through the machine states/possibilities. Please LMK if there are any questions.

To attempt and get ahead, I added a final state submitSuccess that's essentially a dupe of the idle state, but the page is using it to know to navigate.

edit-schedule-model

Copy link
Member

Choose a reason for hiding this comment

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

This was so helpful; thank you!

import { assign, createMachine } from "xstate"
import * as API from "../../api/api"
import { ApiError, FieldErrors, mapApiErrorToFieldErrors } from "../../api/errors"
import * as TypesGen from "../../api/typesGenerated"
import { displayError, displaySuccess } from "../../components/GlobalSnackbar/utils"

export const Language = {
errorSubmissionFailed: "Failed to update schedule",
errorWorkspaceFetch: "Failed to fetch workspace",
successMessage: "Successfully updated workspace schedule.",
}

export interface WorkspaceScheduleContext {
formErrors?: FieldErrors
getWorkspaceError?: Error | unknown
/**
* Each workspace has their own schedule (start and ttl). For this reason, we
* re-fetch the workspace to ensure we're up-to-date. As a result, this
* machine is partially influenced by workspaceXService.
*/
workspace?: TypesGen.Workspace
}

export type WorkspaceScheduleEvent =
| { type: "GET_WORKSPACE"; workspaceId: string }
| {
type: "SUBMIT_SCHEDULE"
autoStart: TypesGen.UpdateWorkspaceAutostartRequest
ttl: TypesGen.UpdateWorkspaceTTLRequest
}

export const workspaceSchedule = createMachine(
{
tsTypes: {} as import("./workspaceScheduleXService.typegen").Typegen0,
schema: {
context: {} as WorkspaceScheduleContext,
events: {} as WorkspaceScheduleEvent,
services: {} as {
getWorkspace: {
data: TypesGen.Workspace
}
},
},
id: "workspaceScheduleState",
initial: "idle",
on: {
GET_WORKSPACE: "gettingWorkspace",
},
states: {
idle: {
tags: "loading",
},
gettingWorkspace: {
entry: ["clearGetWorkspaceError", "clearContext"],
invoke: {
src: "getWorkspace",
id: "getWorkspace",
onDone: {
target: "presentForm",
actions: ["assignWorkspace"],
},
onError: {
target: "error",
actions: ["assignGetWorkspaceError", "displayWorkspaceError"],
},
},
tags: "loading",
},
presentForm: {
on: {
SUBMIT_SCHEDULE: "submittingSchedule",
},
},
submittingSchedule: {
invoke: {
src: "submitSchedule",
id: "submitSchedule",
onDone: {
target: "idle",
actions: "displaySuccess",
},
onError: {
target: "presentForm",
actions: ["assignSubmissionError", "displaySubmissionError"],
},
},
tags: "loading",
},
error: {
on: {
GET_WORKSPACE: "gettingWorkspace",
},
},
},
},
{
actions: {
assignSubmissionError: assign({
formErrors: (_, event) => mapApiErrorToFieldErrors((event.data as ApiError).response.data),
}),
assignWorkspace: assign({
workspace: (_, event) => event.data,
}),
assignGetWorkspaceError: assign({
getWorkspaceError: (_, event) => event.data,
}),
clearContext: () => {
assign({ workspace: undefined })
},
clearGetWorkspaceError: (context) => {
assign({ ...context, getWorkspaceError: undefined })
},
displayWorkspaceError: () => {
displayError(Language.errorWorkspaceFetch)
},
displaySubmissionError: () => {
displayError(Language.errorSubmissionFailed)
},
displaySuccess: () => {
displaySuccess(Language.successMessage)
},
},

services: {
getWorkspace: async (_, event) => {
return await API.getWorkspace(event.workspaceId)
},
submitSchedule: async (context, event) => {
if (!context.workspace?.id) {
// This state is theoretically impossible, but helps TS
throw new Error("failed to load workspace")
}

// REMARK: These calls are purposefully synchronous because if one
// value contradicts the other, we don't want a race condition
// on re-submission.
await API.putWorkspaceAutostart(context.workspace.id, event.autoStart)
await API.putWorkspaceAutostop(context.workspace.id, event.ttl)
},
},
},
)