chore(site): remove workspace schedule machine (#10583)

Related to https://github.com/coder/coder/issues/9943
This commit is contained in:
Bruno Quaresma
2023-11-08 13:46:29 -03:00
committed by GitHub
parent 26740cf00d
commit 71153e2317
2 changed files with 114 additions and 302 deletions
@@ -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;
const error = checkPermissionsError || getTemplateError;
const isLoading = !template || !permissions;
if (!username || !workspaceName) {
return <Navigate to="/workspaces" />;
}
if (scheduleState.matches("done")) {
return <Navigate to={`/@${username}/${workspaceName}`} />;
}
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 = () => {
>
<PageHeaderTitle>Workspace Schedule</PageHeaderTitle>
</PageHeader>
{(scheduleState.hasTag("loading") || !template) && <Loader />}
{scheduleState.matches("error") && (
<ErrorAlert error={checkPermissionsError || getTemplateError} />
)}
{error && <ErrorAlert error={error} />}
{isLoading && <Loader />}
{permissions && !permissions.updateWorkspace && (
<Alert severity="error">
You don&apos;t have permissions to update the schedule for this
workspace.
</Alert>
)}
{template &&
workspace &&
(scheduleState.matches("presentForm") ||
scheduleState.matches("submittingSchedule")) && (
<WorkspaceScheduleForm
enableAutoStart={template.allow_user_autostart}
enableAutoStop={template.allow_user_autostop}
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={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 && (
<WorkspaceScheduleForm
enableAutoStart={template.allow_user_autostart}
enableAutoStop={template.allow_user_autostop}
submitScheduleError={submitScheduleMutation.error}
initialValues={{
...getAutostart(workspace),
...getAutostop(workspace),
}}
isLoading={submitScheduleMutation.isLoading}
defaultTTL={dayjs.duration(template.default_ttl_ms, "ms").asHours()}
onCancel={() => {
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);
}
}}
/>
)}
<ConfirmDialog
open={scheduleState.matches("showingRestartDialog")}
open={isConfirmingApply}
title="Restart workspace?"
description="Would you like to restart your workspace now to apply your new autostop setting, or let it apply after your next workspace start?"
confirmText="Restart"
cancelText="Apply later"
hideCancel={false}
onConfirm={() => {
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<void>[] = [];
if (autostartChanged) {
actions.push(putWorkspaceAutostart(workspace.id, autostart));
}
if (autostopChanged) {
actions.push(putWorkspaceAutostop(workspace.id, ttl));
}
return Promise.all(actions);
};
export default WorkspaceSchedulePage;
@@ -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<keyof ReturnType<typeof permissionsToCheck>, 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);
}
},
},
},
);