diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx
index 08d116744e4a6..b54edcf437e61 100644
--- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx
+++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx
@@ -1,4 +1,3 @@
-import { useMachine } from "@xstate/react";
import { Alert } from "components/Alert/Alert";
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
import { Loader } from "components/Loader/Loader";
@@ -10,25 +9,38 @@ import {
} from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule";
import { ttlMsToAutostop } from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/ttl";
import { useWorkspaceSettings } from "pages/WorkspaceSettingsPage/WorkspaceSettingsLayout";
-import { FC } from "react";
+import { FC, useState } from "react";
import { Helmet } from "react-helmet-async";
-import { Navigate, useNavigate, useParams } from "react-router-dom";
+import { useNavigate, useParams } from "react-router-dom";
import { pageTitle } from "utils/page";
import * as TypesGen from "api/typesGenerated";
import { workspaceByOwnerAndNameKey } from "api/queries/workspaces";
import { WorkspaceScheduleForm } from "./WorkspaceScheduleForm";
-import { workspaceSchedule } from "xServices/workspaceSchedule/workspaceScheduleXService";
import {
formValuesToAutostartRequest,
formValuesToTTLRequest,
} from "./formToRequest";
import { ErrorAlert } from "components/Alert/ErrorAlert";
-import { useQueryClient } from "react-query";
+import { useMutation, useQuery, useQueryClient } from "react-query";
+import { checkAuthorization } from "api/queries/authCheck";
+import { templateByName } from "api/queries/templates";
+import {
+ putWorkspaceAutostart,
+ putWorkspaceAutostop,
+ startWorkspace,
+} from "api/api";
-const getAutostart = (workspace: TypesGen.Workspace) =>
- scheduleToAutostart(workspace.autostart_schedule);
-const getAutostop = (workspace: TypesGen.Workspace) =>
- ttlMsToAutostop(workspace.ttl_ms);
+const permissionsToCheck = (workspace: TypesGen.Workspace) =>
+ ({
+ updateWorkspace: {
+ object: {
+ resource_type: "workspace",
+ resource_id: workspace.id,
+ owner_id: workspace.owner_id,
+ },
+ action: "update",
+ },
+ }) as const;
export const WorkspaceSchedulePage: FC = () => {
const params = useParams() as { username: string; workspace: string };
@@ -37,24 +49,28 @@ export const WorkspaceSchedulePage: FC = () => {
const workspaceName = params.workspace;
const queryClient = useQueryClient();
const workspace = useWorkspaceSettings();
- const [scheduleState, scheduleSend] = useMachine(workspaceSchedule, {
- context: { workspace },
+ const { data: permissions, error: checkPermissionsError } = useQuery(
+ checkAuthorization({ checks: permissionsToCheck(workspace) }),
+ );
+ const { data: template, error: getTemplateError } = useQuery(
+ templateByName(workspace.organization_id, workspace.template_name),
+ );
+ const submitScheduleMutation = useMutation({
+ mutationFn: submitSchedule,
+ onSuccess: async () => {
+ await queryClient.invalidateQueries(
+ workspaceByOwnerAndNameKey(params.username, params.workspace),
+ );
+ },
});
- const {
- checkPermissionsError,
- submitScheduleError,
- getTemplateError,
- permissions,
- template,
- } = scheduleState.context;
-
- if (!username || !workspaceName) {
- return ;
- }
+ const error = checkPermissionsError || getTemplateError;
+ const isLoading = !template || !permissions;
- if (scheduleState.matches("done")) {
- return ;
- }
+ const [isConfirmingApply, setIsConfirmingApply] = useState(false);
+ const { mutate: updateWorkspace } = useMutation({
+ mutationFn: () =>
+ startWorkspace(workspace.id, workspace.template_active_version_id),
+ });
return (
<>
@@ -68,70 +84,99 @@ export const WorkspaceSchedulePage: FC = () => {
>
Workspace Schedule
- {(scheduleState.hasTag("loading") || !template) && }
- {scheduleState.matches("error") && (
-
- )}
+
+ {error && }
+
+ {isLoading && }
+
{permissions && !permissions.updateWorkspace && (
You don't have permissions to update the schedule for this
workspace.
)}
- {template &&
- workspace &&
- (scheduleState.matches("presentForm") ||
- scheduleState.matches("submittingSchedule")) && (
- {
- navigate(`/@${username}/${workspaceName}`);
- }}
- onSubmit={async (values) => {
- scheduleSend({
- type: "SUBMIT_SCHEDULE",
- autostart: formValuesToAutostartRequest(values),
- ttl: formValuesToTTLRequest(values),
- autostartChanged: scheduleChanged(
- getAutostart(workspace),
- values,
- ),
- autostopChanged: scheduleChanged(
- getAutostop(workspace),
- values,
- ),
- });
-
- await queryClient.invalidateQueries(
- workspaceByOwnerAndNameKey(params.username, params.workspace),
- );
- }}
- />
- )}
+
+ {template && (
+ {
+ navigate(`/@${username}/${workspaceName}`);
+ }}
+ onSubmit={async (values) => {
+ const data = {
+ workspace,
+ autostart: formValuesToAutostartRequest(values),
+ ttl: formValuesToTTLRequest(values),
+ autostartChanged: scheduleChanged(
+ getAutostart(workspace),
+ values,
+ ),
+ autostopChanged: scheduleChanged(getAutostop(workspace), values),
+ };
+
+ await submitScheduleMutation.mutateAsync(data);
+
+ if (data.autostopChanged) {
+ setIsConfirmingApply(true);
+ }
+ }}
+ />
+ )}
+
{
- scheduleSend("RESTART_WORKSPACE");
+ updateWorkspace();
+ navigate(`/@${username}/${workspaceName}`);
}}
onClose={() => {
- scheduleSend("APPLY_LATER");
+ navigate(`/@${username}/${workspaceName}`);
}}
/>
>
);
};
+const getAutostart = (workspace: TypesGen.Workspace) =>
+ scheduleToAutostart(workspace.autostart_schedule);
+
+const getAutostop = (workspace: TypesGen.Workspace) =>
+ ttlMsToAutostop(workspace.ttl_ms);
+
+type SubmitScheduleData = {
+ workspace: TypesGen.Workspace;
+ autostart: TypesGen.UpdateWorkspaceAutostartRequest;
+ autostartChanged: boolean;
+ ttl: TypesGen.UpdateWorkspaceTTLRequest;
+ autostopChanged: boolean;
+};
+
+const submitSchedule = async (data: SubmitScheduleData) => {
+ const { autostartChanged, workspace, autostart, autostopChanged, ttl } = data;
+ const actions: Promise[] = [];
+
+ if (autostartChanged) {
+ actions.push(putWorkspaceAutostart(workspace.id, autostart));
+ }
+
+ if (autostopChanged) {
+ actions.push(putWorkspaceAutostop(workspace.id, ttl));
+ }
+
+ return Promise.all(actions);
+};
+
export default WorkspaceSchedulePage;
diff --git a/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts b/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts
deleted file mode 100644
index 9d14f790e0249..0000000000000
--- a/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts
+++ /dev/null
@@ -1,233 +0,0 @@
-/**
- * @fileoverview workspaceSchedule is an xstate machine backing a form to CRUD
- * an individual workspace's schedule.
- */
-import { assign, createMachine } from "xstate";
-import * as API from "api/api";
-import * as TypesGen from "api/typesGenerated";
-
-type Permissions = Record, boolean>;
-
-export interface WorkspaceScheduleContext {
- getWorkspaceError?: 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;
- template?: TypesGen.Template;
- getTemplateError?: unknown;
- permissions?: Permissions;
- checkPermissionsError?: unknown;
- submitScheduleError?: unknown;
- autostopChanged?: boolean;
- shouldRestartWorkspace?: boolean;
-}
-
-export const checks = {
- updateWorkspace: "updateWorkspace",
-} as const;
-
-const permissionsToCheck = (workspace: TypesGen.Workspace) =>
- ({
- [checks.updateWorkspace]: {
- object: {
- resource_type: "workspace",
- resource_id: workspace.id,
- owner_id: workspace.owner_id,
- },
- action: "update",
- },
- }) as const;
-
-export type WorkspaceScheduleEvent =
- | {
- type: "SUBMIT_SCHEDULE";
- autostart: TypesGen.UpdateWorkspaceAutostartRequest;
- autostartChanged: boolean;
- ttl: TypesGen.UpdateWorkspaceTTLRequest;
- autostopChanged: boolean;
- }
- | { type: "RESTART_WORKSPACE" }
- | { 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 {
- getTemplate: {
- data: TypesGen.Template;
- };
- },
- },
- initial: "gettingPermissions",
- states: {
- gettingPermissions: {
- entry: "clearGetPermissionsError",
- invoke: {
- src: "checkPermissions",
- id: "checkPermissions",
- onDone: [
- {
- actions: ["assignPermissions"],
- target: "gettingTemplate",
- },
- ],
- onError: [
- {
- actions: "assignGetPermissionsError",
- target: "error",
- },
- ],
- },
- tags: "loading",
- },
- gettingTemplate: {
- entry: "clearGetTemplateError",
- invoke: {
- src: "getTemplate",
- id: "getTemplate",
- onDone: {
- target: "presentForm",
- actions: ["assignTemplate"],
- },
- onError: {
- target: "error",
- actions: ["assignGetTemplateError"],
- },
- },
- tags: "loading",
- },
- presentForm: {
- on: {
- SUBMIT_SCHEDULE: {
- target: "submittingSchedule",
- actions: "assignAutostopChanged",
- },
- },
- },
- submittingSchedule: {
- invoke: {
- src: "submitSchedule",
- id: "submitSchedule",
- onDone: [
- {
- cond: "autostopChanged",
- target: "showingRestartDialog",
- },
- { target: "done" },
- ],
- onError: {
- target: "presentForm",
- actions: ["assignSubmissionError"],
- },
- },
- tags: "loading",
- },
- showingRestartDialog: {
- on: {
- RESTART_WORKSPACE: {
- target: "done",
- actions: "restartWorkspace",
- },
- APPLY_LATER: "done",
- },
- },
- error: {
- type: "final",
- },
- done: {
- type: "final",
- },
- },
- },
- {
- guards: {
- autostopChanged: (context) => Boolean(context.autostopChanged),
- },
- actions: {
- assignSubmissionError: assign({
- submitScheduleError: (_, 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,
- }),
- assignTemplate: assign({
- template: (_, event) => event.data,
- }),
- assignGetTemplateError: assign({
- getTemplateError: (_, event) => event.data,
- }),
- clearGetTemplateError: assign({
- getTemplateError: (_) => undefined,
- }),
- assignAutostopChanged: assign({
- autostopChanged: (_, event) => event.autostopChanged,
- }),
- clearGetPermissionsError: assign({
- checkPermissionsError: (_) => 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: {
- 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({
- 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);
- }
- },
- },
- },
- );