From e3fb8cc9fddf631e492145950d3c66c81cfed2ab Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Wed, 7 Dec 2022 21:27:33 +0000 Subject: [PATCH 01/18] Update xstate machine --- .../workspaceScheduleXService.ts | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts b/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts index 33382097dd535..ced272a312e75 100644 --- a/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts +++ b/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts @@ -5,7 +5,6 @@ import { assign, createMachine } from "xstate" import * as API from "../../api/api" import * as TypesGen from "../../api/typesGenerated" -import { displaySuccess } from "../../components/GlobalSnackbar/utils" export const Language = { successMessage: "Successfully updated workspace schedule.", @@ -24,6 +23,8 @@ export interface WorkspaceScheduleContext { permissions?: Permissions checkPermissionsError?: Error | unknown submitScheduleError?: Error | unknown + autoStopChanged?: boolean + shouldRestartWorkspace?: boolean } export const checks = { @@ -50,8 +51,12 @@ export type WorkspaceScheduleEvent = ttl: TypesGen.UpdateWorkspaceTTLRequest autoStopChanged: boolean } + | { type: "RESTART_WORKSPACE" } + | { type: "APPLY_LATER" } -export const workspaceSchedule = createMachine( +export const workspaceSchedule = +/** @xstate-layout N4IgpgJg5mDOIC5QHcD2AnA1rADgQwGMwBlAgC0gFcAbEgFzzrAGIBxAUQBUB9AdQHkASgGliABQCCAYXYBtAAwBdRKBypYASzobUAOxUgAHogCMAJgB0AVgAcAThsAWeXZMA2O2YDsdq1YA0IACeiGbyFl428o4AzFYmMT4xbl5mAL5pgWhYuIQk5FS0xAxMFjB02rpQvBjY+ETMEHpgFhq6AG6omC3lNTn1YArKSCBqmtp6BsYI7tbybmZ+9mYO8u6OgSEIMY6OFo5eC34mdqkLNhlZtblEpBQQNPSMPWAVbdXXA8xg6OgYFjhqIwAGYYAC2ZVefTqeSGBjGWh0+hG02SNmsXhMVhWdkcbkcNm8Xk2oRcFgW7hi8h8djcVkuIGyMNuBQeRRKLzeVTEPzBGlgmj0sEazVaHS6LQKBEwPPQfIFSNgcJGCImyNA0wAtOEYrjsa4vD5bMkEiSEG44uTFosCa41lY3AymTd8vdHsVnpCuVBZfLBbphT8-ugAUC6KC5RYpTLefz-UqlPD1IjJijEMkrBE3G4bO5cfN5GYzGadnsDgt5rmnFZHL4nZ88ndCk9Sjh0HAwLo6AAxcHMYgAVQAQgBZACSPGIUgAEuwACIDgAyckTKuTaqmiEzqTMdOzJixJi82OLwUQNic5MxrjxusS5nr-UbrPdHIssEoACM+d6m2yWE0ugtG0nTdO+X4-n+jzKqo65IpuMx7CYF7ZjYMSJLihpeBsZ4zO4Jj7HahomPINaEo4j7Mq6zYeqUH7flolRQFBtAikBYqgS09GQS+tCyCYwyweM8Fpggjg1hYu5UrqZiOCsjjmGaB5uARtYkfq7jUjYjqZIyDYsm67KetxjHvCxLBBv8gIguC4EMXQ5kwaMcGphq6YpBEB4JFYuqxN4Jhmq4bj7CkiReNEpykSYlEuuZtFcWQqDIO8ghwAw6B0HOGh4NQqBQMwgjsMQnASIIPACCI4jSCugnOcJrlGOeMT7LYyExMhClybESmJMFrU2OFDo+OYOlXE+Bk0W+sCJclVSpbA6WZdluX5RIYhiIuACa3CLhInDsIITmqiJbnbG4Oq1gpaxOHmMRKWY2kWPYObnehOwxDFAxxW+lnoGwXB8EIoiSDIR0ueqjXbFYdgWKcVZmCN5x+EpBJPa4ngmLE8jtV4GS6boqAQHABjOl9vEtmASb1RDWqo75GmGr4aFuGampYpYTgI-IUS+Nzhy47ppPPoZFOtBAtBUymNOmOEUQ1nidJWMe1IPazdIWDsXMHseXgxFEAtjVR32euUTHQi6ksbqJXgWPI8y1rJix23ESso3sdvRLrKxmDEjvpIL+nUf+8VekxvpxoqlsnZD6K5nYLhHnJNjYmsZoEjbuu+ASJqdbrn3C5Nnpth2Xa9nKUcNdMuxPdjaFOFS2ErGakTNQjDu3qkJF2PnE3B1NEGmVU5kV9LMzhNDSvzBa1IqZ4OFbLSMMWj7OZoXrhIfQH41B6+xkzSlaV4BlWU5XlI8ISReyYbJ5h2A4cnI7hI2ZtmcSRNpOZxP7huxeTIe-efUSthMyyU8PHO+R4LRKQks4HMmMDhfzWNFLeRs-5vkApTNc1MEK6hAe4c6pE6QDTCCzJ+tZY4nD1qkdCqQBYZCAA */ +createMachine( { id: "workspaceScheduleState", predictableActionArguments: true, @@ -110,17 +115,20 @@ export const workspaceSchedule = createMachine( }, presentForm: { on: { - SUBMIT_SCHEDULE: "submittingSchedule", + SUBMIT_SCHEDULE: { target: "submittingSchedule", actions: "assignAutoStopChanged" }, }, }, submittingSchedule: { invoke: { src: "submitSchedule", id: "submitSchedule", - onDone: { - target: "submitSuccess", - actions: "displaySuccess", - }, + onDone: [ + { + cond: "autoStartChanged", + target: "showingRestartDialog", + }, + { target: "done" } + ], onError: { target: "presentForm", actions: ["assignSubmissionError"], @@ -128,9 +136,10 @@ export const workspaceSchedule = createMachine( }, tags: "loading", }, - submitSuccess: { + showingRestartDialog: { on: { - SUBMIT_SCHEDULE: "submittingSchedule", + RESTART_WORKSPACE: { target: "done", actions: "assignRestartWorkspace" }, + APPLY_LATER: "done" }, }, error: { @@ -138,9 +147,13 @@ export const workspaceSchedule = createMachine( GET_WORKSPACE: "gettingWorkspace", }, }, + done: {} }, }, { + guards: { + autoStartChanged: (context) => Boolean(context.autoStopChanged) + }, actions: { assignSubmissionError: assign({ submitScheduleError: (_, event) => event.data, @@ -159,6 +172,12 @@ export const workspaceSchedule = createMachine( assignGetPermissionsError: assign({ checkPermissionsError: (_, event) => event.data, }), + assignAutoStopChanged: assign({ + autoStopChanged: (_) => true + }), + assignRestartWorkspace: assign({ + shouldRestartWorkspace: (_) => true + }), clearGetPermissionsError: assign({ checkPermissionsError: (_) => undefined, }), @@ -168,9 +187,6 @@ export const workspaceSchedule = createMachine( clearGetWorkspaceError: (context) => { assign({ ...context, getWorkspaceError: undefined }) }, - displaySuccess: () => { - displaySuccess(Language.successMessage) - }, }, services: { From 86b695dc05bc735f7f7a18720a7c5cd5b74f4f1a Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Wed, 7 Dec 2022 22:21:25 +0000 Subject: [PATCH 02/18] Fix autoStopChanged --- .../workspaceScheduleXService.ts | 301 +++++++++--------- 1 file changed, 155 insertions(+), 146 deletions(-) diff --git a/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts b/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts index ced272a312e75..407e3806122a0 100644 --- a/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts +++ b/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts @@ -55,171 +55,180 @@ export type WorkspaceScheduleEvent = | { type: "APPLY_LATER" } export const workspaceSchedule = -/** @xstate-layout N4IgpgJg5mDOIC5QHcD2AnA1rADgQwGMwBlAgC0gFcAbEgFzzrAGIBxAUQBUB9AdQHkASgGliABQCCAYXYBtAAwBdRKBypYASzobUAOxUgAHogCMAJgB0AVgAcAThsAWeXZMA2O2YDsdq1YA0IACeiGbyFl428o4AzFYmMT4xbl5mAL5pgWhYuIQk5FS0xAxMFjB02rpQvBjY+ETMEHpgFhq6AG6omC3lNTn1YArKSCBqmtp6BsYI7tbybmZ+9mYO8u6OgSEIMY6OFo5eC34mdqkLNhlZtblEpBQQNPSMPWAVbdXXA8xg6OgYFjhqIwAGYYAC2ZVefTqeSGBjGWh0+hG02SNmsXhMVhWdkcbkcNm8Xk2oRcFgW7hi8h8djcVkuIGyMNuBQeRRKLzeVTEPzBGlgmj0sEazVaHS6LQKBEwPPQfIFSNgcJGCImyNA0wAtOEYrjsa4vD5bMkEiSEG44uTFosCa41lY3AymTd8vdHsVnpCuVBZfLBbphT8-ugAUC6KC5RYpTLefz-UqlPD1IjJijEMkrBE3G4bO5cfN5GYzGadnsDgt5rmnFZHL4nZ88ndCk9Sjh0HAwLo6AAxcHMYgAVQAQgBZACSPGIUgAEuwACIDgAyckTKuTaqmiEzqTMdOzJixJi82OLwUQNic5MxrjxusS5nr-UbrPdHIssEoACM+d6m2yWE0ugtG0nTdO+X4-n+jzKqo65IpuMx7CYF7ZjYMSJLihpeBsZ4zO4Jj7HahomPINaEo4j7Mq6zYeqUH7flolRQFBtAikBYqgS09GQS+tCyCYwyweM8Fpggjg1hYu5UrqZiOCsjjmGaB5uARtYkfq7jUjYjqZIyDYsm67KetxjHvCxLBBv8gIguC4EMXQ5kwaMcGphq6YpBEB4JFYuqxN4Jhmq4bj7CkiReNEpykSYlEuuZtFcWQqDIO8ghwAw6B0HOGh4NQqBQMwgjsMQnASIIPACCI4jSCugnOcJrlGOeMT7LYyExMhClybESmJMFrU2OFDo+OYOlXE+Bk0W+sCJclVSpbA6WZdluX5RIYhiIuACa3CLhInDsIITmqiJbnbG4Oq1gpaxOHmMRKWY2kWPYObnehOwxDFAxxW+lnoGwXB8EIoiSDIR0ueqjXbFYdgWKcVZmCN5x+EpBJPa4ngmLE8jtV4GS6boqAQHABjOl9vEtmASb1RDWqo75GmGr4aFuGampYpYTgI-IUS+Nzhy47ppPPoZFOtBAtBUymNOmOEUQ1nidJWMe1IPazdIWDsXMHseXgxFEAtjVR32euUTHQi6ksbqJXgWPI8y1rJix23ESso3sdvRLrKxmDEjvpIL+nUf+8VekxvpxoqlsnZD6K5nYLhHnJNjYmsZoEjbuu+ASJqdbrn3C5Nnpth2Xa9nKUcNdMuxPdjaFOFS2ErGakTNQjDu3qkJF2PnE3B1NEGmVU5kV9LMzhNDSvzBa1IqZ4OFbLSMMWj7OZoXrhIfQH41B6+xkzSlaV4BlWU5XlI8ISReyYbJ5h2A4cnI7hI2ZtmcSRNpOZxP7huxeTIe-efUSthMyyU8PHO+R4LRKQks4HMmMDhfzWNFLeRs-5vkApTNc1MEK6hAe4c6pE6QDTCCzJ+tZY4nD1qkdCqQBYZCAA */ -createMachine( - { - id: "workspaceScheduleState", - predictableActionArguments: true, - tsTypes: {} as import("./workspaceScheduleXService.typegen").Typegen0, - schema: { - context: {} as WorkspaceScheduleContext, - events: {} as WorkspaceScheduleEvent, - services: {} as { - getWorkspace: { - data: TypesGen.Workspace - } + /** @xstate-layout N4IgpgJg5mDOIC5QHcD2AnA1rADgQwGMwBlAgC0gFcAbEgFzzrAGIBxAUQBUB9AdQHkASgGliABQCCAYXYBtAAwBdRKBypYASzobUAOxUgAHogCMAJgB0AVgAcAThsAWeXZMA2O2YDsdq1YA0IACeiGbyFl428o4AzFYmMT4xbl5mAL5pgWhYuIQk5FS0xAxMFjB02rpQvBjY+ETMEHpgFhq6AG6omC3lNTn1YArKSCBqmtp6BsYI7tbybmZ+9mYO8u6OgSEIMY6OFo5eC34mdqkLNhlZtblEpBQQNPSMPWAVbdXXA8xg6OgYFjhqIwAGYYAC2ZVefTqeSGBjGWh0+hG02SNmsXhMVhWdkcbkcNm8Xk2oRcFgW7hi8h8djcVkuIGyMNuBQeRRKLzeVTEPzBGlgmj0sEazVaHS6LQKBEwPPQfIFSNgcJGCImyNA0wAtOEYrjsa4vD5bMkEiSEG44uTFosCa41lY3AymTd8vdHsVnpCuVBZfLBbphT8-ugAUC6KC5RYpTLefz-UqlPD1IjJijEMkrBE3G4bO5cfN5GYzGadnsDgt5rmnFZHL4nZ88ndCk9Sjh0HAwLo6AAxcHMYgAVQAQgBZACSPGIUgAEuwACIDgAyckTKuTaqmiEzqTMdOzJixJi82OLwUQNic5MxrjxusS5nr-UbrPdHIssEoACM+d6m2yWE0ugtG0nTdO+X4-n+jzKqo65IpuMx7CYF7ZjYMSJLihpeBsZ4zO4Jj7HahomPINaEo4j7Mq6zYeqUH7flolRQFBtAikBYqgS09GQS+tCyCYwyweM8Fpggjg1hYu5UrqZiOCsjjmGaB5uARtYkfq7jUjYjqZIyDYsm67KetxjHvCxLBBv8gIguC4EMXQ5kwaMcGphq6YpBEB4JFYuqxN4Jhmq4bj7CkiReNEpykSYlEuuZtFcWQqDIO8ghwAw6B0HOGh4NQqBQMwgjsMQnASIIPACCI4jSCugnOcJrlGOeMT7LYyExMhClybESmJMFrU2OFDo+OYOlXE+Bk0W+sCJclVSpbA6WZdluX5RIYhiIuACa3CLhInDsIITmqiJbnbG4Oq1gpaxOHmMRKWY2kWPYObnehOwxDFAxxW+lnoGwXB8EIoiSDIR0ueqjXbFYdgWKcVZmCN5x+EpBJPa4ngmLE8jtV4GS6boqAQHABjOl9vEtmASb1RDWqo75GmGr4aFuGampYpYTgI-IUS+Nzhy47ppPPoZFOtBAtBUymNOmOEUQ1nidJWMe1IPazdIWDsXMHseXgxFEAtjVR32euUTHQi6ksbqJXgWPI8y1rJix23ESso3sdvRLrKxmDEjvpIL+nUf+8VekxvpxoqlsnZD6K5nYLhHnJNjYmsZoEjbuu+ASJqdbrn3C5Nnpth2Xa9nKUcNdMuxPdjaFOFS2ErGakTNQjDu3qkJF2PnE3B1NEGmVU5kV9LMzhNDSvzBa1IqZ4OFbLSMMWj7OZoXrhIfQH41B6+xkzSlaV4BlWU5XlI8ISReyYbJ5h2A4cnI7hI2ZtmcSRNpOZxP7huxeTIe-efUSthMyyU8PHO+R4LRKQks4HMmMDhfzWNFLeRs-5vkApTNc1MEK6hAe4c6pE6QDTCCzJ+tZY4nD1qkdCqQBYZCAA */ + createMachine( + { + id: "workspaceScheduleState", + predictableActionArguments: true, + tsTypes: {} as import("./workspaceScheduleXService.typegen").Typegen0, + schema: { + context: {} as WorkspaceScheduleContext, + events: {} as WorkspaceScheduleEvent, + services: {} as { + getWorkspace: { + data: TypesGen.Workspace + } + }, }, - }, - initial: "idle", - on: { - GET_WORKSPACE: "gettingWorkspace", - }, - states: { - idle: { - tags: "loading", + initial: "idle", + on: { + GET_WORKSPACE: "gettingWorkspace", }, - gettingWorkspace: { - entry: ["clearGetWorkspaceError", "clearContext"], - invoke: { - src: "getWorkspace", - id: "getWorkspace", - onDone: { - target: "gettingPermissions", - actions: ["assignWorkspace"], - }, - onError: { - target: "error", - actions: ["assignGetWorkspaceError"], - }, + states: { + idle: { + tags: "loading", }, - tags: "loading", - }, - gettingPermissions: { - entry: "clearGetPermissionsError", - invoke: { - src: "checkPermissions", - id: "checkPermissions", - onDone: [ - { - actions: ["assignPermissions"], - target: "presentForm", + gettingWorkspace: { + entry: ["clearGetWorkspaceError", "clearContext"], + invoke: { + src: "getWorkspace", + id: "getWorkspace", + onDone: { + target: "gettingPermissions", + actions: ["assignWorkspace"], }, - ], - onError: [ - { - actions: "assignGetPermissionsError", + onError: { target: "error", + actions: ["assignGetWorkspaceError"], }, - ], + }, + tags: "loading", }, - }, - presentForm: { - on: { - SUBMIT_SCHEDULE: { target: "submittingSchedule", actions: "assignAutoStopChanged" }, + gettingPermissions: { + entry: "clearGetPermissionsError", + invoke: { + src: "checkPermissions", + id: "checkPermissions", + onDone: [ + { + actions: ["assignPermissions"], + target: "presentForm", + }, + ], + onError: [ + { + actions: "assignGetPermissionsError", + target: "error", + }, + ], + }, }, - }, - submittingSchedule: { - invoke: { - src: "submitSchedule", - id: "submitSchedule", - onDone: [ - { - cond: "autoStartChanged", - target: "showingRestartDialog", + presentForm: { + on: { + SUBMIT_SCHEDULE: { + target: "submittingSchedule", + actions: "assignAutoStopChanged", }, - { target: "done" } - ], - onError: { - target: "presentForm", - actions: ["assignSubmissionError"], }, }, - tags: "loading", - }, - showingRestartDialog: { - on: { - RESTART_WORKSPACE: { target: "done", actions: "assignRestartWorkspace" }, - APPLY_LATER: "done" + submittingSchedule: { + invoke: { + src: "submitSchedule", + id: "submitSchedule", + onDone: [ + { + cond: "autoStartChanged", + target: "showingRestartDialog", + }, + { target: "done" }, + ], + onError: { + target: "presentForm", + actions: ["assignSubmissionError"], + }, + }, + tags: "loading", }, - }, - error: { - on: { - GET_WORKSPACE: "gettingWorkspace", + showingRestartDialog: { + on: { + RESTART_WORKSPACE: { + target: "done", + actions: "assignRestartWorkspace", + }, + APPLY_LATER: "done", + }, + }, + error: { + on: { + GET_WORKSPACE: "gettingWorkspace", + }, }, + done: {}, }, - done: {} }, - }, - { - guards: { - autoStartChanged: (context) => Boolean(context.autoStopChanged) - }, - actions: { - 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. - permissions: (_, event) => event.data as Permissions, - }), - assignGetPermissionsError: assign({ - checkPermissionsError: (_, event) => event.data, - }), - assignAutoStopChanged: assign({ - autoStopChanged: (_) => true - }), - assignRestartWorkspace: assign({ - shouldRestartWorkspace: (_) => true - }), - clearGetPermissionsError: assign({ - checkPermissionsError: (_) => undefined, - }), - clearContext: () => { - assign({ workspace: undefined, permissions: undefined }) + { + guards: { + autoStartChanged: (context) => Boolean(context.autoStopChanged), }, - clearGetWorkspaceError: (context) => { - assign({ ...context, getWorkspaceError: undefined }) + actions: { + 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. + permissions: (_, event) => event.data as Permissions, + }), + assignGetPermissionsError: assign({ + checkPermissionsError: (_, event) => event.data, + }), + assignAutoStopChanged: assign({ + autoStopChanged: (_, event) => event.autoStopChanged, + }), + assignRestartWorkspace: assign({ + shouldRestartWorkspace: (_) => true, + }), + clearGetPermissionsError: assign({ + checkPermissionsError: (_) => undefined, + }), + clearContext: () => { + assign({ workspace: undefined, permissions: undefined }) + }, + clearGetWorkspaceError: (context) => { + assign({ ...context, getWorkspaceError: undefined }) + }, }, - }, - services: { - getWorkspace: async (_, event) => { - return await API.getWorkspaceByOwnerAndName( - event.username, - event.workspaceName, - ) - }, - checkPermissions: async (context) => { - if (context.workspace) { - return await API.checkAuthorization({ - checks: permissionsToCheck(context.workspace), - }) - } else { - throw Error( - "Cannot check permissions without both workspace and user id", + services: { + getWorkspace: async (_, event) => { + return await API.getWorkspaceByOwnerAndName( + event.username, + event.workspaceName, ) - } - }, - submitSchedule: async (context, event) => { - if (!context.workspace?.id) { - // This state is theoretically impossible, but helps TS - throw new Error("Failed to load workspace.") - } + }, + checkPermissions: async (context) => { + if (context.workspace) { + return await API.checkAuthorization({ + checks: permissionsToCheck(context.workspace), + }) + } else { + throw Error( + "Cannot check permissions without both workspace and user id", + ) + } + }, + submitSchedule: async (context, event) => { + if (!context.workspace?.id) { + // This state is theoretically impossible, but helps TS + throw new Error("Failed to load workspace.") + } - if (event.autoStartChanged) { - await API.putWorkspaceAutostart(context.workspace.id, event.autoStart) - } - if (event.autoStopChanged) { - await API.putWorkspaceAutostop(context.workspace.id, event.ttl) - } + if (event.autoStartChanged) { + await API.putWorkspaceAutostart( + context.workspace.id, + event.autoStart, + ) + } + if (event.autoStopChanged) { + await API.putWorkspaceAutostop(context.workspace.id, event.ttl) + } + }, }, }, - }, -) + ) From 9408e24bbff0b4f43f2037dd6d08eaadfae663d4 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Wed, 7 Dec 2022 22:21:30 +0000 Subject: [PATCH 03/18] Add dialog --- .../WorkspaceSchedulePage.tsx | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx index ad48166dea9c7..00e11e6695eec 100644 --- a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx +++ b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx @@ -1,5 +1,6 @@ import { useMachine } from "@xstate/react" import { AlertBanner } from "components/AlertBanner/AlertBanner" +import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog" import { scheduleToAutoStart } from "pages/WorkspaceSchedulePage/schedule" import { ttlMsToAutoStop } from "pages/WorkspaceSchedulePage/ttl" import React, { useEffect, useState } from "react" @@ -20,6 +21,11 @@ const Language = { "You don't have permissions to update the schedule for this workspace.", getWorkspaceError: "Failed to fetch workspace.", checkPermissionsError: "Failed to fetch permissions.", + dialogTitle: "Restart workspace?", + dialogDescription: `Would you like to restart your workspace now to apply your new auto-stop setting, + or let it apply after your next workspace start?`, + restart: "Restart workspace now", + applyLater: "Apply update later", } export const WorkspaceSchedulePage: React.FC = () => { @@ -35,6 +41,7 @@ export const WorkspaceSchedulePage: React.FC = () => { getWorkspaceError, permissions, workspace, + shouldRestartWorkspace, } = scheduleState.context // Get workspace on mount and whenever the args for getting a workspace change. @@ -119,8 +126,32 @@ export const WorkspaceSchedulePage: React.FC = () => { ) } - if (scheduleState.matches("submitSuccess")) { - return + if (scheduleState.matches("showingRestartDialog")) { + return ( + { + scheduleSend("RESTART_WORKSPACE") + }} + onClose={() => { + scheduleSend("APPLY_LATER") + }} + /> + ) + } + + if (scheduleState.matches("done")) { + return ( + + ) } // Theoretically impossible - log and bail From 09d2214c41a6d1f5551857075d6e57a0aaa51a13 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Wed, 7 Dec 2022 22:59:08 +0000 Subject: [PATCH 04/18] Restart workspace --- site/src/pages/WorkspacePage/WorkspacePage.tsx | 4 +++- site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index a1882c4422c5e..01e620812b5e7 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -3,7 +3,7 @@ import { useMachine } from "@xstate/react" import { AlertBanner } from "components/AlertBanner/AlertBanner" import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" import { FC, useEffect } from "react" -import { useParams } from "react-router-dom" +import { useLocation, useParams } from "react-router-dom" import { Loader } from "components/Loader/Loader" import { firstOrItem } from "util/array" import { workspaceMachine } from "xServices/workspace/workspaceXService" @@ -15,6 +15,7 @@ export const WorkspacePage: FC = () => { useParams() const username = firstOrItem(usernameQueryParam, null) const workspaceName = firstOrItem(workspaceQueryParam, null) + const shouldRestartWorkspace = useLocation().state?.shouldRestartWorkspace ?? false const [workspaceState, workspaceSend] = useMachine(workspaceMachine) const { workspace, @@ -69,6 +70,7 @@ export const WorkspacePage: FC = () => { workspaceState={workspaceState} quotaState={quotaState} workspaceSend={workspaceSend} + shouldRestartWorkspace={shouldRestartWorkspace} /> diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index ab293db819533..e2dba63dde531 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -30,12 +30,14 @@ interface WorkspaceReadyPageProps { workspaceState: StateFrom quotaState: StateFrom workspaceSend: (event: WorkspaceEvent) => void + shouldRestartWorkspace: boolean } export const WorkspaceReadyPage = ({ workspaceState, quotaState, workspaceSend, + shouldRestartWorkspace }: WorkspaceReadyPageProps): JSX.Element => { const [bannerState, bannerSend] = useActor( workspaceState.children["scheduleBannerMachine"], @@ -71,6 +73,13 @@ export const WorkspaceReadyPage = ({ bannerSend({ type: "REFRESH_WORKSPACE", workspace }) }, [bannerSend, workspace]) + useEffect(() => { + // workspaces can be started without being stopped first + shouldRestartWorkspace && workspaceSend({ type: "START" }) + // clear location state + window.history.replaceState({}, document.title) + }, [shouldRestartWorkspace, workspaceSend]) + return ( <> From 1729f2b5c8e80ac67faabeca0e53fa6de5243650 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Thu, 8 Dec 2022 17:44:25 +0000 Subject: [PATCH 05/18] Clearing location doesn't work and doesn't seem necessary --- site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index e2dba63dde531..ef03307492579 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -37,7 +37,7 @@ export const WorkspaceReadyPage = ({ workspaceState, quotaState, workspaceSend, - shouldRestartWorkspace + shouldRestartWorkspace, }: WorkspaceReadyPageProps): JSX.Element => { const [bannerState, bannerSend] = useActor( workspaceState.children["scheduleBannerMachine"], @@ -76,8 +76,6 @@ export const WorkspaceReadyPage = ({ useEffect(() => { // workspaces can be started without being stopped first shouldRestartWorkspace && workspaceSend({ type: "START" }) - // clear location state - window.history.replaceState({}, document.title) }, [shouldRestartWorkspace, workspaceSend]) return ( From cf0b8fdf338687aa0480068187d07a7b4f6579fd Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Mon, 12 Dec 2022 18:28:57 +0000 Subject: [PATCH 06/18] Fix test --- .../src/pages/WorkspacePage/WorkspacePage.tsx | 3 ++- .../WorkspaceSchedulePage.test.tsx | 23 +++++++++++++++++++ .../WorkspaceSchedulePage.tsx | 2 +- site/src/testHelpers/handlers.ts | 1 + 4 files changed, 27 insertions(+), 2 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index 01e620812b5e7..ab3b86955172f 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -15,7 +15,8 @@ export const WorkspacePage: FC = () => { useParams() const username = firstOrItem(usernameQueryParam, null) const workspaceName = firstOrItem(workspaceQueryParam, null) - const shouldRestartWorkspace = useLocation().state?.shouldRestartWorkspace ?? false + const shouldRestartWorkspace = + useLocation().state?.shouldRestartWorkspace ?? false const [workspaceState, workspaceSend] = useMachine(workspaceMachine) const { workspace, diff --git a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx index f44a89dbc466c..1e8de1f21399c 100644 --- a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx +++ b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx @@ -1,3 +1,6 @@ +import { MockUser, MockWorkspace, renderWithAuth } from "testHelpers/renderHelpers" +import userEvent from "@testing-library/user-event" +import { screen } from "@testing-library/react" import { formValuesToAutoStartRequest, formValuesToTTLRequest, @@ -241,4 +244,24 @@ describe("WorkspaceSchedulePage", () => { expect(ttlMsToAutoStop(ttlMs)).toEqual(autoStop) }) }) + + describe("autoStop change dialog", () => { + it("shows if autoStop is changed", async () => { + renderWithAuth(, { + route: `/@${MockUser.username}/${MockWorkspace.name}/schedule`, + path: "/@:username/:workspace/schedule" + }) + const user = userEvent.setup() + const autoStopToggle = await screen.findByLabelText( + FormLanguage.stopSwitch, + ) + await user.click(autoStopToggle) + const submitButton = await screen.findByRole("button", { name: /submit/i }) + await user.click(submitButton) + const dialog = await screen.findByText(PageLanguage.dialogTitle) + expect(dialog).toBeInTheDocument() + }) + + + }) }) diff --git a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx index 00e11e6695eec..e3cde241e0feb 100644 --- a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx +++ b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx @@ -16,7 +16,7 @@ import { formValuesToTTLRequest, } from "./formToRequest" -const Language = { +export const Language = { forbiddenError: "You don't have permissions to update the schedule for this workspace.", getWorkspaceError: "Failed to fetch workspace.", diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts index 3c44d2ba942f4..408cf8528d5a4 100644 --- a/site/src/testHelpers/handlers.ts +++ b/site/src/testHelpers/handlers.ts @@ -125,6 +125,7 @@ export const handlers = [ const permissions = [ ...Object.keys(permissionsToCheck), "canUpdateTemplate", + "updateWorkspace" ] const response = permissions.reduce((obj, permission) => { return { From 0c4d52c60c716b42ebfd46683fc498572bc70fef Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Mon, 12 Dec 2022 20:13:40 +0000 Subject: [PATCH 07/18] Fix second test --- .../WorkspaceSchedulePage.test.tsx | 25 ++++++++++++++++-- .../WorkspaceSchedulePage.tsx | 26 +++++++------------ .../workspaceScheduleXService.ts | 1 + 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx index 1e8de1f21399c..027c2190c3cc6 100644 --- a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx +++ b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx @@ -11,7 +11,14 @@ import { } from "pages/WorkspaceSchedulePage/schedule" import { AutoStop, ttlMsToAutoStop } from "pages/WorkspaceSchedulePage/ttl" import * as TypesGen from "../../api/typesGenerated" -import { WorkspaceScheduleFormValues } from "../../components/WorkspaceScheduleForm/WorkspaceScheduleForm" +import { + WorkspaceScheduleFormValues, + Language as FormLanguage, +} from "../../components/WorkspaceScheduleForm/WorkspaceScheduleForm" +import { + WorkspaceSchedulePage, + Language as PageLanguage, +} from "./WorkspaceSchedulePage" const validValues: WorkspaceScheduleFormValues = { autoStartEnabled: true, @@ -262,6 +269,20 @@ describe("WorkspaceSchedulePage", () => { expect(dialog).toBeInTheDocument() }) - + it("doesn't show if autoStop is not changed", async () => { + renderWithAuth(, { + route: `/@${MockUser.username}/${MockWorkspace.name}/schedule`, + path: "/@:username/:workspace/schedule" + }) + const user = userEvent.setup() + const autoStartToggle = await screen.findByLabelText( + FormLanguage.startSwitch, + ) + await user.click(autoStartToggle) + const submitButton = await screen.findByRole("button", { name: /submit/i }) + await user.click(submitButton) + const dialog = screen.queryByText(PageLanguage.dialogTitle) + expect(dialog).not.toBeInTheDocument() + }) }) }) diff --git a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx index e3cde241e0feb..5a386775eb5ee 100644 --- a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx +++ b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx @@ -3,7 +3,7 @@ import { AlertBanner } from "components/AlertBanner/AlertBanner" import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog" import { scheduleToAutoStart } from "pages/WorkspaceSchedulePage/schedule" import { ttlMsToAutoStop } from "pages/WorkspaceSchedulePage/ttl" -import React, { useEffect, useState } from "react" +import React, { useEffect } from "react" import { Navigate, useNavigate, useParams } from "react-router-dom" import { scheduleChanged } from "util/schedule" import * as TypesGen from "../../api/typesGenerated" @@ -28,6 +28,11 @@ export const Language = { applyLater: "Apply update later", } +const getAutoStart = (workspace?: TypesGen.Workspace) => + scheduleToAutoStart(workspace?.autostart_schedule) +const getAutoStop = (workspace?: TypesGen.Workspace) => + ttlMsToAutoStop(workspace?.ttl_ms) + export const WorkspaceSchedulePage: React.FC = () => { const { username: usernameQueryParam, workspace: workspaceQueryParam } = useParams() @@ -52,19 +57,6 @@ export const WorkspaceSchedulePage: React.FC = () => { 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]) - if (!username || !workspaceName) { return } @@ -108,7 +100,7 @@ export const WorkspaceSchedulePage: React.FC = () => { return ( { navigate(`/@${username}/${workspaceName}`) @@ -118,8 +110,8 @@ export const WorkspaceSchedulePage: React.FC = () => { type: "SUBMIT_SCHEDULE", autoStart: formValuesToAutoStartRequest(values), ttl: formValuesToTTLRequest(values), - autoStartChanged: scheduleChanged(autoStart, values), - autoStopChanged: scheduleChanged(autoStop, values), + autoStartChanged: scheduleChanged(getAutoStart(workspace), values), + autoStopChanged: scheduleChanged(getAutoStop(workspace), values), }) }} /> diff --git a/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts b/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts index 407e3806122a0..3f318f4f44c32 100644 --- a/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts +++ b/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts @@ -214,6 +214,7 @@ export const workspaceSchedule = } }, submitSchedule: async (context, event) => { + console.log(event) if (!context.workspace?.id) { // This state is theoretically impossible, but helps TS throw new Error("Failed to load workspace.") From 700593048a32ee662d9c27220a10cb29c45f56f4 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Mon, 12 Dec 2022 20:15:00 +0000 Subject: [PATCH 08/18] Format --- .../WorkspaceSchedulePage.test.tsx | 18 +++++++++++++----- .../WorkspaceSchedulePage.tsx | 5 ++++- site/src/testHelpers/handlers.ts | 2 +- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx index 027c2190c3cc6..6a80d7e1c44c1 100644 --- a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx +++ b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx @@ -1,4 +1,8 @@ -import { MockUser, MockWorkspace, renderWithAuth } from "testHelpers/renderHelpers" +import { + MockUser, + MockWorkspace, + renderWithAuth, +} from "testHelpers/renderHelpers" import userEvent from "@testing-library/user-event" import { screen } from "@testing-library/react" import { @@ -256,14 +260,16 @@ describe("WorkspaceSchedulePage", () => { it("shows if autoStop is changed", async () => { renderWithAuth(, { route: `/@${MockUser.username}/${MockWorkspace.name}/schedule`, - path: "/@:username/:workspace/schedule" + path: "/@:username/:workspace/schedule", }) const user = userEvent.setup() const autoStopToggle = await screen.findByLabelText( FormLanguage.stopSwitch, ) await user.click(autoStopToggle) - const submitButton = await screen.findByRole("button", { name: /submit/i }) + const submitButton = await screen.findByRole("button", { + name: /submit/i, + }) await user.click(submitButton) const dialog = await screen.findByText(PageLanguage.dialogTitle) expect(dialog).toBeInTheDocument() @@ -272,14 +278,16 @@ describe("WorkspaceSchedulePage", () => { it("doesn't show if autoStop is not changed", async () => { renderWithAuth(, { route: `/@${MockUser.username}/${MockWorkspace.name}/schedule`, - path: "/@:username/:workspace/schedule" + path: "/@:username/:workspace/schedule", }) const user = userEvent.setup() const autoStartToggle = await screen.findByLabelText( FormLanguage.startSwitch, ) await user.click(autoStartToggle) - const submitButton = await screen.findByRole("button", { name: /submit/i }) + const submitButton = await screen.findByRole("button", { + name: /submit/i, + }) await user.click(submitButton) const dialog = screen.queryByText(PageLanguage.dialogTitle) expect(dialog).not.toBeInTheDocument() diff --git a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx index 5a386775eb5ee..8a9b1fcb0ffce 100644 --- a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx +++ b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx @@ -100,7 +100,10 @@ export const WorkspaceSchedulePage: React.FC = () => { return ( { navigate(`/@${username}/${workspaceName}`) diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts index 408cf8528d5a4..19631b0492df4 100644 --- a/site/src/testHelpers/handlers.ts +++ b/site/src/testHelpers/handlers.ts @@ -125,7 +125,7 @@ export const handlers = [ const permissions = [ ...Object.keys(permissionsToCheck), "canUpdateTemplate", - "updateWorkspace" + "updateWorkspace", ] const response = permissions.reduce((obj, permission) => { return { From b19837d32773ba8bf2ba4fa5a49fcae97e37df94 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Mon, 12 Dec 2022 20:54:07 +0000 Subject: [PATCH 09/18] Lint --- .../src/xServices/workspaceSchedule/workspaceScheduleXService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts b/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts index 3f318f4f44c32..407e3806122a0 100644 --- a/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts +++ b/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts @@ -214,7 +214,6 @@ export const workspaceSchedule = } }, submitSchedule: async (context, event) => { - console.log(event) if (!context.workspace?.id) { // This state is theoretically impossible, but helps TS throw new Error("Failed to load workspace.") From da30f5bcfc045cae98ffc6c7b0a2952833142fa9 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Tue, 13 Dec 2022 21:03:58 +0000 Subject: [PATCH 10/18] Use i18n --- site/src/i18n/en/index.ts | 2 ++ site/src/i18n/en/workspaceSchedulePage.json | 9 ++++++ .../WorkspaceSchedulePage.test.tsx | 10 +++++-- .../WorkspaceSchedulePage.tsx | 30 ++++++------------- 4 files changed, 27 insertions(+), 24 deletions(-) create mode 100644 site/src/i18n/en/workspaceSchedulePage.json diff --git a/site/src/i18n/en/index.ts b/site/src/i18n/en/index.ts index 37b8b66dca66b..3be9daf125ed6 100644 --- a/site/src/i18n/en/index.ts +++ b/site/src/i18n/en/index.ts @@ -12,6 +12,7 @@ import templateSettingsPage from "./templateSettingsPage.json" import templateVersionPage from "./templateVersionPage.json" import loginPage from "./loginPage.json" import workspaceChangeVersionPage from "./workspaceChangeVersionPage.json" +import workspaceSchedulePage from "./workspaceSchedulePage.json" import serviceBannerSettings from "./serviceBannerSettings.json" export const en = { @@ -29,5 +30,6 @@ export const en = { templateVersionPage, loginPage, workspaceChangeVersionPage, + workspaceSchedulePage, serviceBannerSettings, } diff --git a/site/src/i18n/en/workspaceSchedulePage.json b/site/src/i18n/en/workspaceSchedulePage.json new file mode 100644 index 0000000000000..85e0d6ae5b6a5 --- /dev/null +++ b/site/src/i18n/en/workspaceSchedulePage.json @@ -0,0 +1,9 @@ +{ + "forbiddenError": "You don't have permissions to update the schedule for this workspace.", + "getWorkspaceError": "Failed to fetch workspace.", + "checkPermissionsError": "Failed to fetch permissions.", + "dialogTitle": "Restart workspace?", + "dialogDescription": "Would you like to restart your workspace now to apply your new auto-stop setting, or let it apply after your next workspace start?", + "restart": "Restart workspace now", + "applyLater": "Apply update later" +} diff --git a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx index 6a80d7e1c44c1..9c4ddd1547a3d 100644 --- a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx +++ b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx @@ -21,8 +21,10 @@ import { } from "../../components/WorkspaceScheduleForm/WorkspaceScheduleForm" import { WorkspaceSchedulePage, - Language as PageLanguage, } from "./WorkspaceSchedulePage" +import i18next from "i18next" + +const { t } = i18next const validValues: WorkspaceScheduleFormValues = { autoStartEnabled: true, @@ -271,7 +273,8 @@ describe("WorkspaceSchedulePage", () => { name: /submit/i, }) await user.click(submitButton) - const dialog = await screen.findByText(PageLanguage.dialogTitle) + const title = t("dialogTitle", { ns: "workspaceSchedulePage" }) + const dialog = await screen.findByText(title) expect(dialog).toBeInTheDocument() }) @@ -289,7 +292,8 @@ describe("WorkspaceSchedulePage", () => { name: /submit/i, }) await user.click(submitButton) - const dialog = screen.queryByText(PageLanguage.dialogTitle) + const title = t("dialogTitle", { ns: "workspaceSchedulePage" }) + const dialog = screen.queryByText(title) expect(dialog).not.toBeInTheDocument() }) }) diff --git a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx index 8a9b1fcb0ffce..6916d094404c1 100644 --- a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx +++ b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx @@ -4,6 +4,7 @@ import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog" import { scheduleToAutoStart } from "pages/WorkspaceSchedulePage/schedule" import { ttlMsToAutoStop } from "pages/WorkspaceSchedulePage/ttl" import React, { useEffect } 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" @@ -16,24 +17,13 @@ import { formValuesToTTLRequest, } from "./formToRequest" -export const Language = { - forbiddenError: - "You don't have permissions to update the schedule for this workspace.", - getWorkspaceError: "Failed to fetch workspace.", - checkPermissionsError: "Failed to fetch permissions.", - dialogTitle: "Restart workspace?", - dialogDescription: `Would you like to restart your workspace now to apply your new auto-stop setting, - or let it apply after your next workspace start?`, - restart: "Restart workspace now", - applyLater: "Apply update later", -} - const getAutoStart = (workspace?: TypesGen.Workspace) => scheduleToAutoStart(workspace?.autostart_schedule) const getAutoStop = (workspace?: TypesGen.Workspace) => ttlMsToAutoStop(workspace?.ttl_ms) export const WorkspaceSchedulePage: React.FC = () => { + const { t } = useTranslation("workspaceSchedulePage") const { username: usernameQueryParam, workspace: workspaceQueryParam } = useParams() const navigate = useNavigate() @@ -77,8 +67,8 @@ export const WorkspaceSchedulePage: React.FC = () => { error={getWorkspaceError || checkPermissionsError} text={ getWorkspaceError - ? Language.getWorkspaceError - : Language.checkPermissionsError + ? t("getWorkspaceError") + : t("checkPermissionsError") } retry={() => scheduleSend({ type: "GET_WORKSPACE", username, workspaceName }) @@ -88,9 +78,7 @@ export const WorkspaceSchedulePage: React.FC = () => { } if (!permissions?.updateWorkspace) { - return ( - - ) + return } if ( @@ -125,10 +113,10 @@ export const WorkspaceSchedulePage: React.FC = () => { return ( { scheduleSend("RESTART_WORKSPACE") From c7376eaa702e950e028551dab7a788ae1b49ede8 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Wed, 14 Dec 2022 19:34:02 +0000 Subject: [PATCH 11/18] Switch to fire and forget restart --- .../src/pages/WorkspacePage/WorkspacePage.tsx | 5 +- .../WorkspacePage/WorkspaceReadyPage.tsx | 7 --- .../WorkspaceSchedulePage.tsx | 3 +- .../workspaceScheduleXService.ts | 51 +++++++++++++++++-- 4 files changed, 48 insertions(+), 18 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index ab3b86955172f..a1882c4422c5e 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -3,7 +3,7 @@ import { useMachine } from "@xstate/react" import { AlertBanner } from "components/AlertBanner/AlertBanner" import { ChooseOne, Cond } from "components/Conditionals/ChooseOne" import { FC, useEffect } from "react" -import { useLocation, useParams } from "react-router-dom" +import { useParams } from "react-router-dom" import { Loader } from "components/Loader/Loader" import { firstOrItem } from "util/array" import { workspaceMachine } from "xServices/workspace/workspaceXService" @@ -15,8 +15,6 @@ export const WorkspacePage: FC = () => { useParams() const username = firstOrItem(usernameQueryParam, null) const workspaceName = firstOrItem(workspaceQueryParam, null) - const shouldRestartWorkspace = - useLocation().state?.shouldRestartWorkspace ?? false const [workspaceState, workspaceSend] = useMachine(workspaceMachine) const { workspace, @@ -71,7 +69,6 @@ export const WorkspacePage: FC = () => { workspaceState={workspaceState} quotaState={quotaState} workspaceSend={workspaceSend} - shouldRestartWorkspace={shouldRestartWorkspace} /> diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index ef03307492579..ab293db819533 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -30,14 +30,12 @@ interface WorkspaceReadyPageProps { workspaceState: StateFrom quotaState: StateFrom workspaceSend: (event: WorkspaceEvent) => void - shouldRestartWorkspace: boolean } export const WorkspaceReadyPage = ({ workspaceState, quotaState, workspaceSend, - shouldRestartWorkspace, }: WorkspaceReadyPageProps): JSX.Element => { const [bannerState, bannerSend] = useActor( workspaceState.children["scheduleBannerMachine"], @@ -73,11 +71,6 @@ export const WorkspaceReadyPage = ({ bannerSend({ type: "REFRESH_WORKSPACE", workspace }) }, [bannerSend, workspace]) - useEffect(() => { - // workspaces can be started without being stopped first - shouldRestartWorkspace && workspaceSend({ type: "START" }) - }, [shouldRestartWorkspace, workspaceSend]) - return ( <> diff --git a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx index 6916d094404c1..c6aec960129f3 100644 --- a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx +++ b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx @@ -36,7 +36,6 @@ export const WorkspaceSchedulePage: React.FC = () => { getWorkspaceError, permissions, workspace, - shouldRestartWorkspace, } = scheduleState.context // Get workspace on mount and whenever the args for getting a workspace change. @@ -55,6 +54,7 @@ export const WorkspaceSchedulePage: React.FC = () => { scheduleState.matches("idle") || scheduleState.matches("gettingWorkspace") || scheduleState.matches("gettingPermissions") || + scheduleState.matches("gettingTemplate") || !workspace ) { return @@ -132,7 +132,6 @@ export const WorkspaceSchedulePage: React.FC = () => { return ( ) } diff --git a/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts b/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts index 407e3806122a0..e3a3d4a5188a8 100644 --- a/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts +++ b/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts @@ -20,6 +20,8 @@ export interface WorkspaceScheduleContext { * machine is partially influenced by workspaceXService. */ workspace?: TypesGen.Workspace + template?: TypesGen.Template + getTemplateError?: Error | unknown permissions?: Permissions checkPermissionsError?: Error | unknown submitScheduleError?: Error | unknown @@ -67,6 +69,9 @@ export const workspaceSchedule = services: {} as { getWorkspace: { data: TypesGen.Workspace + }, + getTemplate: { + data: TypesGen.Template } }, }, @@ -102,7 +107,7 @@ export const workspaceSchedule = onDone: [ { actions: ["assignPermissions"], - target: "presentForm", + target: "gettingTemplate", }, ], onError: [ @@ -113,6 +118,22 @@ export const workspaceSchedule = ], }, }, + gettingTemplate: { + entry: "clearGetTemplateError", + invoke: { + src: "getTemplate", + id: "getTemplate", + onDone: { + target: "presentForm", + actions: ["assignTemplate"], + }, + onError: { + target: "error", + actions: ["assignGetTemplateError"], + }, + }, + tags: "loading", + }, presentForm: { on: { SUBMIT_SCHEDULE: { @@ -143,7 +164,7 @@ export const workspaceSchedule = on: { RESTART_WORKSPACE: { target: "done", - actions: "assignRestartWorkspace", + actions: "restartWorkspace", }, APPLY_LATER: "done", }, @@ -178,12 +199,18 @@ export const workspaceSchedule = assignGetPermissionsError: assign({ checkPermissionsError: (_, event) => event.data, }), + assignTemplate: assign({ + template: (_, event) => event.data + }), + assignGetTemplateError: assign({ + getTemplateError: (_, event) => event.data + }), + clearGetTemplateError: assign({ + getTemplateError: (_) => undefined + }), assignAutoStopChanged: assign({ autoStopChanged: (_, event) => event.autoStopChanged, }), - assignRestartWorkspace: assign({ - shouldRestartWorkspace: (_) => true, - }), clearGetPermissionsError: assign({ checkPermissionsError: (_) => undefined, }), @@ -193,6 +220,13 @@ export const workspaceSchedule = 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) => { + if (context.workspace && context.template) { + return API.startWorkspace(context.workspace.id, context.template.active_version_id) + } + } }, services: { @@ -202,6 +236,13 @@ export const workspaceSchedule = event.workspaceName, ) }, + getTemplate: async (context) => { + if (context.workspace) { + return await API.getTemplate(context.workspace.template_id) + } else { + throw Error("Can't fetch template without workspace.") + } + }, checkPermissions: async (context) => { if (context.workspace) { return await API.checkAuthorization({ From 9a267d60bfb539a2bccb9311d3aa1e14c1d497b5 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Wed, 14 Dec 2022 20:08:49 +0000 Subject: [PATCH 12/18] Improve error handling --- site/src/i18n/en/workspaceSchedulePage.json | 2 - .../WorkspaceSchedulePage.test.tsx | 4 +- .../WorkspaceSchedulePage.tsx | 55 +++++++++++-------- .../workspaceScheduleXService.ts | 16 ++++-- 4 files changed, 43 insertions(+), 34 deletions(-) diff --git a/site/src/i18n/en/workspaceSchedulePage.json b/site/src/i18n/en/workspaceSchedulePage.json index 85e0d6ae5b6a5..abfa221efa362 100644 --- a/site/src/i18n/en/workspaceSchedulePage.json +++ b/site/src/i18n/en/workspaceSchedulePage.json @@ -1,7 +1,5 @@ { "forbiddenError": "You don't have permissions to update the schedule for this workspace.", - "getWorkspaceError": "Failed to fetch workspace.", - "checkPermissionsError": "Failed to fetch permissions.", "dialogTitle": "Restart workspace?", "dialogDescription": "Would you like to restart your workspace now to apply your new auto-stop setting, or let it apply after your next workspace start?", "restart": "Restart workspace now", diff --git a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx index 9c4ddd1547a3d..aabb5956a952a 100644 --- a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx +++ b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx @@ -19,9 +19,7 @@ import { WorkspaceScheduleFormValues, Language as FormLanguage, } from "../../components/WorkspaceScheduleForm/WorkspaceScheduleForm" -import { - WorkspaceSchedulePage, -} from "./WorkspaceSchedulePage" +import { WorkspaceSchedulePage } from "./WorkspaceSchedulePage" import i18next from "i18next" const { t } = i18next diff --git a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx index c6aec960129f3..e13c8703eabbf 100644 --- a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx +++ b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx @@ -1,6 +1,8 @@ +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 { Margins } from "components/Margins/Margins" import { scheduleToAutoStart } from "pages/WorkspaceSchedulePage/schedule" import { ttlMsToAutoStop } from "pages/WorkspaceSchedulePage/ttl" import React, { useEffect } from "react" @@ -22,8 +24,15 @@ const getAutoStart = (workspace?: TypesGen.Workspace) => const getAutoStop = (workspace?: TypesGen.Workspace) => ttlMsToAutoStop(workspace?.ttl_ms) +const useStyles = makeStyles((theme) => ({ + topMargin: { + marginTop: `${theme.spacing(3)}px`, + }, +})) + export const WorkspaceSchedulePage: React.FC = () => { const { t } = useTranslation("workspaceSchedulePage") + const styles = useStyles() const { username: usernameQueryParam, workspace: workspaceQueryParam } = useParams() const navigate = useNavigate() @@ -34,6 +43,7 @@ export const WorkspaceSchedulePage: React.FC = () => { checkPermissionsError, submitScheduleError, getWorkspaceError, + getTemplateError, permissions, workspace, } = scheduleState.context @@ -51,34 +61,37 @@ export const WorkspaceSchedulePage: React.FC = () => { } if ( - scheduleState.matches("idle") || - scheduleState.matches("gettingWorkspace") || - scheduleState.matches("gettingPermissions") || - scheduleState.matches("gettingTemplate") || - !workspace + scheduleState.hasTag("loading") ) { return } if (scheduleState.matches("error")) { return ( - - scheduleSend({ type: "GET_WORKSPACE", username, workspaceName }) - } - /> + +
+ + scheduleSend({ type: "GET_WORKSPACE", username, workspaceName }) + } + /> +
+
) } if (!permissions?.updateWorkspace) { - return + return ( + +
+ +
+
+ ) } if ( @@ -129,11 +142,7 @@ export const WorkspaceSchedulePage: React.FC = () => { } if (scheduleState.matches("done")) { - return ( - - ) + return } // Theoretically impossible - log and bail diff --git a/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts b/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts index e3a3d4a5188a8..d9a651642057b 100644 --- a/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts +++ b/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts @@ -69,7 +69,7 @@ export const workspaceSchedule = services: {} as { getWorkspace: { data: TypesGen.Workspace - }, + } getTemplate: { data: TypesGen.Template } @@ -117,6 +117,7 @@ export const workspaceSchedule = }, ], }, + tags: "loading" }, gettingTemplate: { entry: "clearGetTemplateError", @@ -200,13 +201,13 @@ export const workspaceSchedule = checkPermissionsError: (_, event) => event.data, }), assignTemplate: assign({ - template: (_, event) => event.data + template: (_, event) => event.data, }), assignGetTemplateError: assign({ - getTemplateError: (_, event) => event.data + getTemplateError: (_, event) => event.data, }), clearGetTemplateError: assign({ - getTemplateError: (_) => undefined + getTemplateError: (_) => undefined, }), assignAutoStopChanged: assign({ autoStopChanged: (_, event) => event.autoStopChanged, @@ -224,9 +225,12 @@ export const workspaceSchedule = // user can return to the workspace page to see the restart restartWorkspace: (context) => { if (context.workspace && context.template) { - return API.startWorkspace(context.workspace.id, context.template.active_version_id) + return API.startWorkspace( + context.workspace.id, + context.template.active_version_id, + ) } - } + }, }, services: { From ab2bfbfd0af0e344ffddefe67ae86c700c93cfa9 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Wed, 14 Dec 2022 20:10:19 +0000 Subject: [PATCH 13/18] Format --- .../src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx index e13c8703eabbf..d17cf95fc0870 100644 --- a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx +++ b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx @@ -60,9 +60,7 @@ export const WorkspaceSchedulePage: React.FC = () => { return } - if ( - scheduleState.hasTag("loading") - ) { + if (scheduleState.hasTag("loading")) { return } From 60d2bef70706210f3d3fb525f739d086660d5cf8 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Wed, 14 Dec 2022 20:10:28 +0000 Subject: [PATCH 14/18] Format --- .../xServices/workspaceSchedule/workspaceScheduleXService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts b/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts index d9a651642057b..bb20bcf37eb55 100644 --- a/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts +++ b/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts @@ -117,7 +117,7 @@ export const workspaceSchedule = }, ], }, - tags: "loading" + tags: "loading", }, gettingTemplate: { entry: "clearGetTemplateError", From 5aa4e3f07273497d8cc552a7a80279fe86b23113 Mon Sep 17 00:00:00 2001 From: Presley Pizzo <1290996+presleyp@users.noreply.github.com> Date: Wed, 14 Dec 2022 16:47:52 -0500 Subject: [PATCH 15/18] Update site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx Co-authored-by: Kira Pilot --- .../pages/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx index aabb5956a952a..a228d399849e0 100644 --- a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx +++ b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx @@ -18,7 +18,7 @@ import * as TypesGen from "../../api/typesGenerated" import { WorkspaceScheduleFormValues, Language as FormLanguage, -} from "../../components/WorkspaceScheduleForm/WorkspaceScheduleForm" +} from "components/WorkspaceScheduleForm/WorkspaceScheduleForm" import { WorkspaceSchedulePage } from "./WorkspaceSchedulePage" import i18next from "i18next" From 7e83daa558579fff62bda88745aa9e2b172e015d Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Tue, 20 Dec 2022 18:34:10 +0000 Subject: [PATCH 16/18] Fix name of guard --- .../xServices/workspaceSchedule/workspaceScheduleXService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts b/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts index bb20bcf37eb55..a00983b60b2c7 100644 --- a/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts +++ b/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts @@ -149,7 +149,7 @@ export const workspaceSchedule = id: "submitSchedule", onDone: [ { - cond: "autoStartChanged", + cond: "autoStopChanged", target: "showingRestartDialog", }, { target: "done" }, @@ -180,7 +180,7 @@ export const workspaceSchedule = }, { guards: { - autoStartChanged: (context) => Boolean(context.autoStopChanged), + autoStopChanged: (context) => Boolean(context.autoStopChanged), }, actions: { assignSubmissionError: assign({ From b05e1b1e7ed8ac220521778602aaffd222528a07 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Tue, 20 Dec 2022 18:34:46 +0000 Subject: [PATCH 17/18] Make done state final --- .../xServices/workspaceSchedule/workspaceScheduleXService.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts b/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts index a00983b60b2c7..409ccb57a2501 100644 --- a/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts +++ b/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts @@ -175,7 +175,9 @@ export const workspaceSchedule = GET_WORKSPACE: "gettingWorkspace", }, }, - done: {}, + done: { + type: "final" + }, }, }, { From bb77e4f748e0269dd2c2a393b737e2fa9f5b2284 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Tue, 20 Dec 2022 19:47:50 +0000 Subject: [PATCH 18/18] Format --- .../xServices/workspaceSchedule/workspaceScheduleXService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts b/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts index 409ccb57a2501..65506a0f70e44 100644 --- a/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts +++ b/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts @@ -176,7 +176,7 @@ export const workspaceSchedule = }, }, done: { - type: "final" + type: "final", }, }, },