diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/Buttons.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/Buttons.tsx index ecfde9e97aa9b..576e5fb84ead2 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/Buttons.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/Buttons.tsx @@ -99,6 +99,21 @@ export const StartButton: FC = ({ ); }; +export const UpdateAndStartButton: FC = ({ + handleAction, +}) => { + return ( + + } + onClick={() => handleAction()} + > + Update and start… + + + ); +}; + export const StopButton: FC = ({ handleAction, loading, @@ -148,16 +163,13 @@ export const RestartButton: FC = ({ ); }; -export const UpdateAndStartButton: FC = ({ +export const UpdateAndRestartButton: FC = ({ handleAction, }) => { return ( - } - onClick={() => handleAction()} - > - Update and start… + } onClick={() => handleAction()}> + Update and restart… ); diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx index 3e663dafba1a9..e4188b7b88041 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx @@ -34,93 +34,128 @@ export const Running: Story = { }, }; -export const Stopping: Story = { +export const RunningUpdateAvailable: Story = { + name: "Running (Update available)", args: { - workspace: Mocks.MockStoppingWorkspace, + workspace: { + ...Mocks.MockWorkspace, + outdated: true, + }, }, }; -export const Stopped: Story = { +export const RunningRequireActiveVersion: Story = { + name: "Running (No required update)", args: { - workspace: Mocks.MockStoppedWorkspace, + workspace: { + ...Mocks.MockWorkspace, + template_require_active_version: true, + }, }, }; -export const Canceling: Story = { +export const RunningUpdateRequired: Story = { + name: "Running (Update Required)", args: { - workspace: Mocks.MockCancelingWorkspace, + workspace: { + ...Mocks.MockWorkspace, + template_require_active_version: true, + outdated: true, + }, }, }; -export const Canceled: Story = { +export const Stopping: Story = { args: { - workspace: Mocks.MockCanceledWorkspace, + workspace: Mocks.MockStoppingWorkspace, }, }; -export const Deleting: Story = { +export const Stopped: Story = { args: { - workspace: Mocks.MockDeletingWorkspace, + workspace: Mocks.MockStoppedWorkspace, }, }; -export const Deleted: Story = { +export const StoppedUpdateAvailable: Story = { + name: "Stopped (Update available)", args: { - workspace: Mocks.MockDeletedWorkspace, + workspace: { + ...Mocks.MockStoppedWorkspace, + outdated: true, + }, }, }; -export const Outdated: Story = { +export const StoppedRequireActiveVersion: Story = { + name: "Stopped (No required update)", + args: { + workspace: { + ...Mocks.MockStoppedWorkspace, + template_require_active_version: true, + }, + }, +}; + +export const StoppedUpdateRequired: Story = { + name: "Stopped (Update Required)", + args: { + workspace: { + ...Mocks.MockStoppedWorkspace, + template_require_active_version: true, + outdated: true, + }, + }, +}; + +export const Updating: Story = { args: { workspace: Mocks.MockOutdatedWorkspace, + isUpdating: true, }, }; -export const Failed: Story = { +export const Restarting: Story = { args: { - workspace: Mocks.MockFailedWorkspace, + workspace: Mocks.MockStoppingWorkspace, + isRestarting: true, }, }; -export const FailedWithDebug: Story = { +export const Canceling: Story = { args: { - workspace: Mocks.MockFailedWorkspace, - canDebug: true, + workspace: Mocks.MockCancelingWorkspace, }, }; -export const Updating: Story = { +export const Deleting: Story = { args: { - isUpdating: true, - workspace: Mocks.MockOutdatedWorkspace, + workspace: Mocks.MockDeletingWorkspace, }, }; -export const RequireActiveVersionStarted: Story = { +export const Deleted: Story = { args: { - workspace: Mocks.MockOutdatedRunningWorkspaceRequireActiveVersion, - canChangeVersions: false, + workspace: Mocks.MockDeletedWorkspace, }, }; -export const RequireActiveVersionStopped: Story = { +export const Outdated: Story = { args: { - workspace: Mocks.MockOutdatedStoppedWorkspaceRequireActiveVersion, - canChangeVersions: false, + workspace: Mocks.MockOutdatedWorkspace, }, }; -export const AlwaysUpdateStarted: Story = { +export const Failed: Story = { args: { - workspace: Mocks.MockOutdatedRunningWorkspaceAlwaysUpdate, - canChangeVersions: true, + workspace: Mocks.MockFailedWorkspace, }, }; -export const AlwaysUpdateStopped: Story = { +export const FailedWithDebug: Story = { args: { - workspace: Mocks.MockOutdatedStoppedWorkspaceAlwaysUpdate, - canChangeVersions: true, + workspace: Mocks.MockFailedWorkspace, + canDebug: true, }, }; @@ -133,6 +168,7 @@ export const CancelShownForOwner: Story = { isOwner: true, }, }; + export const CancelShownForUser: Story = { args: { workspace: Mocks.MockStartingWorkspace, diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx index 86a53d592243e..e58f8190e900f 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx @@ -26,6 +26,7 @@ import { ActivateButton, FavoriteButton, UpdateAndStartButton, + UpdateAndRestartButton, } from "./Buttons"; import { type ActionType, abilitiesByWorkspaceStatus } from "./constants"; import { DebugButton } from "./DebugButton"; @@ -89,12 +90,12 @@ export const WorkspaceActions: FC = ({ const mustUpdate = mustUpdateWorkspace(workspace, canChangeVersions); const tooltipText = getTooltipText(workspace, mustUpdate, canChangeVersions); - const canBeUpdated = workspace.outdated && canAcceptJobs; // A mapping of button type to the corresponding React component const buttonMapping: Record = { update: , updateAndStart: , + updateAndRestart: , updating: , start: ( = ({ enableBuildParameters={workspace.latest_build.transition === "start"} /> ), - toggleFavorite: ( - - ), }; return ( @@ -166,30 +160,22 @@ export const WorkspaceActions: FC = ({ css={{ display: "flex", alignItems: "center", gap: 8 }} data-testid="workspace-actions" > - {canBeUpdated && ( - <> - {isUpdating - ? buttonMapping.updating - : workspace.template_require_active_version - ? buttonMapping.updateAndStart - : buttonMapping.update} - - )} - - {!canBeUpdated && - !isUpdating && - workspace.template_require_active_version && - buttonMapping.start} - - {isRestarting - ? buttonMapping.restarting - : actions.map((action) => ( - {buttonMapping[action]} - ))} + {/* Restarting must be handled separately, because it otherwise would appear as stopping */} + {isUpdating + ? buttonMapping.updating + : isRestarting + ? buttonMapping.restarting + : actions.map((action) => ( + {buttonMapping[action]} + ))} {showCancel && } - {buttonMapping.toggleFavorite} + diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/constants.ts b/site/src/pages/WorkspacePage/WorkspaceActions/constants.ts index c2a85da8cb121..f6d9f8f1cfa20 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/constants.ts +++ b/site/src/pages/WorkspacePage/WorkspaceActions/constants.ts @@ -6,16 +6,19 @@ import type { Workspace } from "api/typesGenerated"; export const actionTypes = [ "start", "starting", + // Replaces start when an update is required. + "updateAndStart", "stop", "stopping", "restart", "restarting", + // Replaces restart when an update is required. + "updateAndRestart", "deleting", "update", "updating", "activate", "activating", - "toggleFavorite", // There's no need for a retrying state because retrying starts a transition // into one of the starting, stopping, or deleting states (based on the @@ -23,10 +26,6 @@ export const actionTypes = [ "retry", "debug", - // When a template requires updates, we aim to display a distinct update - // button that clearly indicates a mandatory update. - "updateAndStart", - // These are buttons that should be used with disabled UI elements "canceling", "deleted", @@ -54,13 +53,6 @@ export const abilitiesByWorkspaceStatus = ( } const status = workspace.latest_build.status; - if (status === "failed" && canDebug) { - return { - actions: ["retry", "debug"], - canCancel: false, - canAcceptJobs: true, - }; - } switch (status) { case "starting": { @@ -73,10 +65,12 @@ export const abilitiesByWorkspaceStatus = ( case "running": { const actions: ActionType[] = ["stop"]; - // If the template requires the latest version, we prevent the user from - // restarting the workspace without updating it first. In the Buttons - // component, we display an UpdateAndStart component to facilitate this. - if (!workspace.template_require_active_version) { + if (workspace.template_require_active_version && workspace.outdated) { + actions.push("updateAndRestart"); + } else { + if (workspace.outdated) { + actions.unshift("update"); + } actions.push("restart"); } @@ -96,10 +90,12 @@ export const abilitiesByWorkspaceStatus = ( case "stopped": { const actions: ActionType[] = []; - // If the template requires the latest version, we prevent the user from - // starting the workspace without updating it first. In the Buttons - // component, we display an UpdateAndStart component to facilitate this. - if (!workspace.template_require_active_version) { + if (workspace.template_require_active_version && workspace.outdated) { + actions.push("updateAndStart"); + } else { + if (workspace.outdated) { + actions.unshift("update"); + } actions.push("start"); } @@ -117,14 +113,31 @@ export const abilitiesByWorkspaceStatus = ( }; } case "failed": { + const actions: ActionType[] = ["retry"]; + + if (canDebug) { + actions.push("debug"); + } + + if (workspace.outdated) { + actions.unshift("update"); + } + return { - actions: ["retry"], + actions, canCancel: false, canAcceptJobs: true, }; } // Disabled states + case "pending": { + return { + actions: ["pending"], + canCancel: false, + canAcceptJobs: false, + }; + } case "canceling": { return { actions: ["canceling"], @@ -146,15 +159,8 @@ export const abilitiesByWorkspaceStatus = ( canAcceptJobs: false, }; } - case "pending": { - return { - actions: ["pending"], - canCancel: false, - canAcceptJobs: false, - }; - } - default: { + + default: throw new Error(`Unknown workspace status: ${status}`); - } } }; diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index bd7627f070fdf..055093570c7ab 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -1175,10 +1175,6 @@ export const MockOutdatedRunningWorkspaceRequireActiveVersion: TypesGen.Workspac id: "test-outdated-workspace-require-active-version", outdated: true, template_require_active_version: true, - latest_build: { - ...MockWorkspaceBuild, - status: "running", - }, }; export const MockOutdatedRunningWorkspaceAlwaysUpdate: TypesGen.Workspace = {