mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore(site): remove workspace schedule machine (#10583)
Related to https://github.com/coder/coder/issues/9943
This commit is contained in:
+114
-69
@@ -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'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);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user