diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 9a8855d0b79be..db7dc1c1ba6c1 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -6746,7 +6746,8 @@ const docTemplate = `{ "create", "start", "stop", - "delete" + "delete", + "restart" ], "allOf": [ { @@ -9564,7 +9565,9 @@ const docTemplate = `{ "canceling", "canceled", "deleting", - "deleted" + "deleted", + "restarting", + "restarted" ], "x-enum-varnames": [ "WorkspaceStatusPending", @@ -9576,7 +9579,9 @@ const docTemplate = `{ "WorkspaceStatusCanceling", "WorkspaceStatusCanceled", "WorkspaceStatusDeleting", - "WorkspaceStatusDeleted" + "WorkspaceStatusDeleted", + "WorkspaceStatusRestarting", + "WorkspaceStatusRestarted" ] }, "codersdk.WorkspaceTransition": { @@ -9584,12 +9589,14 @@ const docTemplate = `{ "enum": [ "start", "stop", - "delete" + "delete", + "restart" ], "x-enum-varnames": [ "WorkspaceTransitionStart", "WorkspaceTransitionStop", - "WorkspaceTransitionDelete" + "WorkspaceTransitionDelete", + "WorkspaceTransitionRestart" ] }, "codersdk.WorkspacesResponse": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index f32e9a87713d0..6d265209aeca3 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -6002,7 +6002,7 @@ "format": "uuid" }, "transition": { - "enum": ["create", "start", "stop", "delete"], + "enum": ["create", "start", "stop", "delete", "restart"], "allOf": [ { "$ref": "#/definitions/codersdk.WorkspaceTransition" @@ -8638,7 +8638,9 @@ "canceling", "canceled", "deleting", - "deleted" + "deleted", + "restarting", + "restarted" ], "x-enum-varnames": [ "WorkspaceStatusPending", @@ -8650,16 +8652,19 @@ "WorkspaceStatusCanceling", "WorkspaceStatusCanceled", "WorkspaceStatusDeleting", - "WorkspaceStatusDeleted" + "WorkspaceStatusDeleted", + "WorkspaceStatusRestarting", + "WorkspaceStatusRestarted" ] }, "codersdk.WorkspaceTransition": { "type": "string", - "enum": ["start", "stop", "delete"], + "enum": ["start", "stop", "delete", "restart"], "x-enum-varnames": [ "WorkspaceTransitionStart", "WorkspaceTransitionStop", - "WorkspaceTransitionDelete" + "WorkspaceTransitionDelete", + "WorkspaceTransitionRestart" ] }, "codersdk.WorkspacesResponse": { diff --git a/coderd/database/dbfake/databasefake.go b/coderd/database/dbfake/databasefake.go index cb4ada860a8a2..bbdf25465ddab 100644 --- a/coderd/database/dbfake/databasefake.go +++ b/coderd/database/dbfake/databasefake.go @@ -1255,6 +1255,22 @@ func (q *fakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database. continue } + case database.WorkspaceStatusRestarted: + if !job.CompletedAt.Valid && + job.CanceledAt.Valid && + job.Error.Valid && + build.Transition != database.WorkspaceTransitionRestart { + continue + } + + case database.WorkspaceStatusRestarting: + if !job.CompletedAt.Valid && + job.CanceledAt.Valid && + job.Error.Valid && + build.Transition != database.WorkspaceTransitionRestart { + continue + } + default: return nil, xerrors.Errorf("unknown workspace status in filter: %q", arg.Status) } diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index f5bbe0cc04a85..4c5f6f205499b 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -125,7 +125,8 @@ CREATE TYPE workspace_app_health AS ENUM ( CREATE TYPE workspace_transition AS ENUM ( 'start', 'stop', - 'delete' + 'delete', + 'restart' ); CREATE TABLE api_keys ( diff --git a/coderd/database/migrations/000117_add_workspace_transition_restart.down.sql b/coderd/database/migrations/000117_add_workspace_transition_restart.down.sql new file mode 100644 index 0000000000000..d1d1637f4fa90 --- /dev/null +++ b/coderd/database/migrations/000117_add_workspace_transition_restart.down.sql @@ -0,0 +1,2 @@ +-- It's not possible to drop enum values from enum types, so the UP has "IF NOT +-- EXISTS". diff --git a/coderd/database/migrations/000117_add_workspace_transition_restart.up.sql b/coderd/database/migrations/000117_add_workspace_transition_restart.up.sql new file mode 100644 index 0000000000000..e0a3e5e512a1e --- /dev/null +++ b/coderd/database/migrations/000117_add_workspace_transition_restart.up.sql @@ -0,0 +1 @@ +ALTER TYPE workspace_transition ADD VALUE IF NOT EXISTS 'restart'; diff --git a/coderd/database/modelmethods.go b/coderd/database/modelmethods.go index 1c20a0cced741..24520fe8b90b1 100644 --- a/coderd/database/modelmethods.go +++ b/coderd/database/modelmethods.go @@ -13,16 +13,18 @@ import ( type WorkspaceStatus string const ( - WorkspaceStatusPending WorkspaceStatus = "pending" - WorkspaceStatusStarting WorkspaceStatus = "starting" - WorkspaceStatusRunning WorkspaceStatus = "running" - WorkspaceStatusStopping WorkspaceStatus = "stopping" - WorkspaceStatusStopped WorkspaceStatus = "stopped" - WorkspaceStatusFailed WorkspaceStatus = "failed" - WorkspaceStatusCanceling WorkspaceStatus = "canceling" - WorkspaceStatusCanceled WorkspaceStatus = "canceled" - WorkspaceStatusDeleting WorkspaceStatus = "deleting" - WorkspaceStatusDeleted WorkspaceStatus = "deleted" + WorkspaceStatusPending WorkspaceStatus = "pending" + WorkspaceStatusStarting WorkspaceStatus = "starting" + WorkspaceStatusRunning WorkspaceStatus = "running" + WorkspaceStatusStopping WorkspaceStatus = "stopping" + WorkspaceStatusStopped WorkspaceStatus = "stopped" + WorkspaceStatusFailed WorkspaceStatus = "failed" + WorkspaceStatusCanceling WorkspaceStatus = "canceling" + WorkspaceStatusCanceled WorkspaceStatus = "canceled" + WorkspaceStatusDeleting WorkspaceStatus = "deleting" + WorkspaceStatusDeleted WorkspaceStatus = "deleted" + WorkspaceStatusRestarting WorkspaceStatus = "restarting" + WorkspaceStatusRestarted WorkspaceStatus = "restarted" ) func (s WorkspaceStatus) Valid() bool { @@ -30,7 +32,7 @@ func (s WorkspaceStatus) Valid() bool { case WorkspaceStatusPending, WorkspaceStatusStarting, WorkspaceStatusRunning, WorkspaceStatusStopping, WorkspaceStatusStopped, WorkspaceStatusFailed, WorkspaceStatusCanceling, WorkspaceStatusCanceled, WorkspaceStatusDeleting, - WorkspaceStatusDeleted: + WorkspaceStatusDeleted, WorkspaceStatusRestarting, WorkspaceStatusRestarted: return true default: return false diff --git a/coderd/database/models.go b/coderd/database/models.go index 529bb413e48f6..1304697c7093d 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1160,9 +1160,10 @@ func AllWorkspaceAppHealthValues() []WorkspaceAppHealth { type WorkspaceTransition string const ( - WorkspaceTransitionStart WorkspaceTransition = "start" - WorkspaceTransitionStop WorkspaceTransition = "stop" - WorkspaceTransitionDelete WorkspaceTransition = "delete" + WorkspaceTransitionStart WorkspaceTransition = "start" + WorkspaceTransitionStop WorkspaceTransition = "stop" + WorkspaceTransitionDelete WorkspaceTransition = "delete" + WorkspaceTransitionRestart WorkspaceTransition = "restart" ) func (e *WorkspaceTransition) Scan(src interface{}) error { @@ -1204,7 +1205,8 @@ func (e WorkspaceTransition) Valid() bool { switch e { case WorkspaceTransitionStart, WorkspaceTransitionStop, - WorkspaceTransitionDelete: + WorkspaceTransitionDelete, + WorkspaceTransitionRestart: return true } return false @@ -1215,6 +1217,7 @@ func AllWorkspaceTransitionValues() []WorkspaceTransition { WorkspaceTransitionStart, WorkspaceTransitionStop, WorkspaceTransitionDelete, + WorkspaceTransitionRestart, } } diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index c6f2e591d5bdd..9eede773997ee 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -318,7 +318,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { switch createBuild.Transition { case codersdk.WorkspaceTransitionDelete: action = rbac.ActionDelete - case codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop: + case codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop, codersdk.WorkspaceTransitionRestart: action = rbac.ActionUpdate default: httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ @@ -588,46 +588,142 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { } } - workspaceBuildID := uuid.New() - input, err := json.Marshal(provisionerdserver.WorkspaceProvisionJob{ - WorkspaceBuildID: workspaceBuildID, - LogLevel: string(createBuild.LogLevel), - }) - if err != nil { - return xerrors.Errorf("marshal provision job: %w", err) - } - provisionerJob, err = db.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{ - ID: uuid.New(), - CreatedAt: database.Now(), - UpdatedAt: database.Now(), - InitiatorID: apiKey.UserID, - OrganizationID: template.OrganizationID, - Provisioner: template.Provisioner, - Type: database.ProvisionerJobTypeWorkspaceBuild, - StorageMethod: templateVersionJob.StorageMethod, - FileID: templateVersionJob.FileID, - Input: input, - Tags: tags, - }) - if err != nil { - return xerrors.Errorf("insert provisioner job: %w", err) - } + var ( + workspaceBuildID uuid.UUID + stopBuildID uuid.UUID + startBuildID uuid.UUID + ) - workspaceBuild, err = db.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{ - ID: workspaceBuildID, - CreatedAt: database.Now(), - UpdatedAt: database.Now(), - WorkspaceID: workspace.ID, - TemplateVersionID: templateVersion.ID, - BuildNumber: priorBuildNum + 1, - ProvisionerState: state, - InitiatorID: apiKey.UserID, - Transition: database.WorkspaceTransition(createBuild.Transition), - JobID: provisionerJob.ID, - Reason: database.BuildReasonInitiator, - }) - if err != nil { - return xerrors.Errorf("insert workspace build: %w", err) + if database.WorkspaceTransition(createBuild.Transition) == database.WorkspaceTransitionRestart { + fmt.Println("in restart block") + stopBuildID = uuid.New() + input, err := json.Marshal(provisionerdserver.WorkspaceProvisionJob{ + WorkspaceBuildID: stopBuildID, + LogLevel: string(createBuild.LogLevel), + }) + if err != nil { + return xerrors.Errorf("marshal provision job: %w", err) + } + + provisionerJob, err = db.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{ + ID: uuid.New(), + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + InitiatorID: apiKey.UserID, + OrganizationID: template.OrganizationID, + Provisioner: template.Provisioner, + Type: database.ProvisionerJobTypeWorkspaceBuild, + StorageMethod: templateVersionJob.StorageMethod, + FileID: templateVersionJob.FileID, + Input: input, + Tags: tags, + }) + if err != nil { + return xerrors.Errorf("insert provisioner job: %w", err) + } + + workspaceBuild, err = db.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{ + ID: stopBuildID, + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + WorkspaceID: workspace.ID, + TemplateVersionID: templateVersion.ID, + BuildNumber: priorBuildNum + 1, + ProvisionerState: state, + InitiatorID: apiKey.UserID, + Transition: database.WorkspaceTransitionStop, + JobID: provisionerJob.ID, + Reason: database.BuildReasonInitiator, + }) + if err != nil { + return xerrors.Errorf("insert workspace build: %w", err) + } + + startBuildID = uuid.New() + input, err = json.Marshal(provisionerdserver.WorkspaceProvisionJob{ + WorkspaceBuildID: startBuildID, + LogLevel: string(createBuild.LogLevel), + }) + if err != nil { + return xerrors.Errorf("marshal provision job: %w", err) + } + + provisionerJob, err = db.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{ + ID: uuid.New(), + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + InitiatorID: apiKey.UserID, + OrganizationID: template.OrganizationID, + Provisioner: template.Provisioner, + Type: database.ProvisionerJobTypeWorkspaceBuild, + StorageMethod: templateVersionJob.StorageMethod, + FileID: templateVersionJob.FileID, + Input: input, + Tags: tags, + }) + if err != nil { + return xerrors.Errorf("insert provisioner job: %w", err) + } + + workspaceBuild, err = db.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{ + ID: startBuildID, + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + WorkspaceID: workspace.ID, + TemplateVersionID: templateVersion.ID, + BuildNumber: priorBuildNum + 2, + ProvisionerState: state, + InitiatorID: apiKey.UserID, + Transition: database.WorkspaceTransitionStart, + JobID: provisionerJob.ID, + Reason: database.BuildReasonInitiator, + }) + if err != nil { + return xerrors.Errorf("insert workspace build: %w", err) + } + } else { + workspaceBuildID = uuid.New() + input, err := json.Marshal(provisionerdserver.WorkspaceProvisionJob{ + WorkspaceBuildID: workspaceBuildID, + LogLevel: string(createBuild.LogLevel), + }) + if err != nil { + return xerrors.Errorf("marshal provision job: %w", err) + } + + provisionerJob, err = db.InsertProvisionerJob(ctx, database.InsertProvisionerJobParams{ + ID: uuid.New(), + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + InitiatorID: apiKey.UserID, + OrganizationID: template.OrganizationID, + Provisioner: template.Provisioner, + Type: database.ProvisionerJobTypeWorkspaceBuild, + StorageMethod: templateVersionJob.StorageMethod, + FileID: templateVersionJob.FileID, + Input: input, + Tags: tags, + }) + if err != nil { + return xerrors.Errorf("insert provisioner job: %w", err) + } + + workspaceBuild, err = db.InsertWorkspaceBuild(ctx, database.InsertWorkspaceBuildParams{ + ID: workspaceBuildID, + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + WorkspaceID: workspace.ID, + TemplateVersionID: templateVersion.ID, + BuildNumber: priorBuildNum + 1, + ProvisionerState: state, + InitiatorID: apiKey.UserID, + Transition: database.WorkspaceTransition(createBuild.Transition), + JobID: provisionerJob.ID, + Reason: database.BuildReasonInitiator, + }) + if err != nil { + return xerrors.Errorf("insert workspace build: %w", err) + } } names := make([]string, 0, len(parameters)) @@ -636,11 +732,25 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { names = append(names, param.Name) values = append(values, param.Value) } - err = db.InsertWorkspaceBuildParameters(ctx, database.InsertWorkspaceBuildParametersParams{ - WorkspaceBuildID: workspaceBuildID, - Name: names, - Value: values, - }) + + if database.WorkspaceTransition(createBuild.Transition) == database.WorkspaceTransitionRestart { + err = db.InsertWorkspaceBuildParameters(ctx, database.InsertWorkspaceBuildParametersParams{ + WorkspaceBuildID: stopBuildID, + Name: names, + Value: values, + }) + err = db.InsertWorkspaceBuildParameters(ctx, database.InsertWorkspaceBuildParametersParams{ + WorkspaceBuildID: startBuildID, + Name: names, + Value: values, + }) + } else { + err = db.InsertWorkspaceBuildParameters(ctx, database.InsertWorkspaceBuildParametersParams{ + WorkspaceBuildID: workspaceBuildID, + Name: names, + Value: values, + }) + } if err != nil { return xerrors.Errorf("insert workspace build parameters: %w", err) } @@ -1191,6 +1301,8 @@ func convertWorkspaceStatus(jobStatus codersdk.ProvisionerJobStatus, transition return codersdk.WorkspaceStatusStopping case codersdk.WorkspaceTransitionDelete: return codersdk.WorkspaceStatusDeleting + case codersdk.WorkspaceTransitionRestart: + return codersdk.WorkspaceStatusRestarting } case codersdk.ProvisionerJobSucceeded: switch transition { @@ -1200,6 +1312,8 @@ func convertWorkspaceStatus(jobStatus codersdk.ProvisionerJobStatus, transition return codersdk.WorkspaceStatusStopped case codersdk.WorkspaceTransitionDelete: return codersdk.WorkspaceStatusDeleted + case codersdk.WorkspaceTransitionRestart: + return codersdk.WorkspaceStatusRestarted } case codersdk.ProvisionerJobCanceling: return codersdk.WorkspaceStatusCanceling diff --git a/codersdk/workspacebuilds.go b/codersdk/workspacebuilds.go index c7bdf022d238f..901f4ce253f08 100644 --- a/codersdk/workspacebuilds.go +++ b/codersdk/workspacebuilds.go @@ -14,24 +14,27 @@ import ( type WorkspaceTransition string const ( - WorkspaceTransitionStart WorkspaceTransition = "start" - WorkspaceTransitionStop WorkspaceTransition = "stop" - WorkspaceTransitionDelete WorkspaceTransition = "delete" + WorkspaceTransitionStart WorkspaceTransition = "start" + WorkspaceTransitionStop WorkspaceTransition = "stop" + WorkspaceTransitionDelete WorkspaceTransition = "delete" + WorkspaceTransitionRestart WorkspaceTransition = "restart" ) type WorkspaceStatus string const ( - WorkspaceStatusPending WorkspaceStatus = "pending" - WorkspaceStatusStarting WorkspaceStatus = "starting" - WorkspaceStatusRunning WorkspaceStatus = "running" - WorkspaceStatusStopping WorkspaceStatus = "stopping" - WorkspaceStatusStopped WorkspaceStatus = "stopped" - WorkspaceStatusFailed WorkspaceStatus = "failed" - WorkspaceStatusCanceling WorkspaceStatus = "canceling" - WorkspaceStatusCanceled WorkspaceStatus = "canceled" - WorkspaceStatusDeleting WorkspaceStatus = "deleting" - WorkspaceStatusDeleted WorkspaceStatus = "deleted" + WorkspaceStatusPending WorkspaceStatus = "pending" + WorkspaceStatusStarting WorkspaceStatus = "starting" + WorkspaceStatusRunning WorkspaceStatus = "running" + WorkspaceStatusStopping WorkspaceStatus = "stopping" + WorkspaceStatusStopped WorkspaceStatus = "stopped" + WorkspaceStatusFailed WorkspaceStatus = "failed" + WorkspaceStatusCanceling WorkspaceStatus = "canceling" + WorkspaceStatusCanceled WorkspaceStatus = "canceled" + WorkspaceStatusDeleting WorkspaceStatus = "deleting" + WorkspaceStatusDeleted WorkspaceStatus = "deleted" + WorkspaceStatusRestarting WorkspaceStatus = "restarting" + WorkspaceStatusRestarted WorkspaceStatus = "restarted" ) type BuildReason string diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 768ebc27c86ee..efaf48097f570 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -55,7 +55,7 @@ const ( // CreateWorkspaceBuildRequest provides options to update the latest workspace build. type CreateWorkspaceBuildRequest struct { TemplateVersionID uuid.UUID `json:"template_version_id,omitempty" format:"uuid"` - Transition WorkspaceTransition `json:"transition" validate:"oneof=create start stop delete,required"` + Transition WorkspaceTransition `json:"transition" validate:"oneof=create start stop delete restart,required"` DryRun bool `json:"dry_run,omitempty"` ProvisionerState []byte `json:"state,omitempty"` // Orphan may be set for the Destroy transition. diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 1a6fed6b99e5a..7d4b54e5841b1 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -1538,13 +1538,14 @@ CreateParameterRequest is a structure used to create a new parameter value for a #### Enumerated Values -| Property | Value | -| ------------ | -------- | -| `log_level` | `debug` | -| `transition` | `create` | -| `transition` | `start` | -| `transition` | `stop` | -| `transition` | `delete` | +| Property | Value | +| ------------ | --------- | +| `log_level` | `debug` | +| `transition` | `create` | +| `transition` | `start` | +| `transition` | `stop` | +| `transition` | `delete` | +| `transition` | `restart` | ## codersdk.CreateWorkspaceProxyRequest @@ -5294,18 +5295,20 @@ Parameter represents a set value for the scope. #### Enumerated Values -| Value | -| ----------- | -| `pending` | -| `starting` | -| `running` | -| `stopping` | -| `stopped` | -| `failed` | -| `canceling` | -| `canceled` | -| `deleting` | -| `deleted` | +| Value | +| ------------ | +| `pending` | +| `starting` | +| `running` | +| `stopping` | +| `stopped` | +| `failed` | +| `canceling` | +| `canceled` | +| `deleting` | +| `deleted` | +| `restarting` | +| `restarted` | ## codersdk.WorkspaceTransition @@ -5317,11 +5320,12 @@ Parameter represents a set value for the scope. #### Enumerated Values -| Value | -| -------- | -| `start` | -| `stop` | -| `delete` | +| Value | +| --------- | +| `start` | +| `stop` | +| `delete` | +| `restart` | ## codersdk.WorkspacesResponse diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 73cb6de377dd0..50be80f0a91cf 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -513,6 +513,14 @@ export const deleteWorkspace = ( transition: "delete", log_level: logLevel, }) +export const restartWorkspace = ( + workspaceId: string, + logLevel?: TypesGen.CreateWorkspaceBuildRequest["log_level"], +) => + postWorkspaceBuild(workspaceId, { + transition: "restart", + log_level: logLevel, + }) export const cancelWorkspaceBuild = async ( workspaceBuildId: TypesGen.WorkspaceBuild["id"], diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index e9eb7ce0d0771..640a3fda1259b 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1532,6 +1532,8 @@ export type WorkspaceStatus = | "deleting" | "failed" | "pending" + | "restarted" + | "restarting" | "running" | "starting" | "stopped" @@ -1543,6 +1545,8 @@ export const WorkspaceStatuses: WorkspaceStatus[] = [ "deleting", "failed", "pending", + "restarted", + "restarting", "running", "starting", "stopped", @@ -1550,9 +1554,10 @@ export const WorkspaceStatuses: WorkspaceStatus[] = [ ] // From codersdk/workspacebuilds.go -export type WorkspaceTransition = "delete" | "start" | "stop" +export type WorkspaceTransition = "delete" | "restart" | "start" | "stop" export const WorkspaceTransitions: WorkspaceTransition[] = [ "delete", + "restart", "start", "stop", ] diff --git a/site/src/components/DropdownButton/ActionCtas.tsx b/site/src/components/DropdownButton/ActionCtas.tsx index 85031e8256ab0..44da6296fdb89 100644 --- a/site/src/components/DropdownButton/ActionCtas.tsx +++ b/site/src/components/DropdownButton/ActionCtas.tsx @@ -7,9 +7,10 @@ import SettingsOutlined from "@material-ui/icons/SettingsOutlined" import HistoryOutlined from "@material-ui/icons/HistoryOutlined" import CropSquareIcon from "@material-ui/icons/CropSquare" import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline" +import ReplayIcon from "@material-ui/icons/Replay" import PlayCircleOutlineIcon from "@material-ui/icons/PlayCircleOutline" import { LoadingButton } from "components/LoadingButton/LoadingButton" -import { FC } from "react" +import { FC, PropsWithChildren } from "react" import { useTranslation } from "react-i18next" import { combineClasses } from "util/combineClasses" import { WorkspaceActionButton } from "../WorkspaceActionButton/WorkspaceActionButton" @@ -18,7 +19,7 @@ interface WorkspaceAction { handleAction: () => void } -export const UpdateButton: FC> = ({ +export const UpdateButton: FC> = ({ handleAction, }) => { const styles = useStyles() @@ -36,7 +37,7 @@ export const UpdateButton: FC> = ({ ) } -export const SettingsButton: FC> = ({ +export const SettingsButton: FC> = ({ handleAction, }) => { const styles = useStyles() @@ -54,9 +55,9 @@ export const SettingsButton: FC> = ({ ) } -export const ChangeVersionButton: FC< - React.PropsWithChildren -> = ({ handleAction }) => { +export const ChangeVersionButton: FC> = ({ + handleAction, +}) => { const styles = useStyles() return ( @@ -71,7 +72,7 @@ export const ChangeVersionButton: FC< ) } -export const StartButton: FC> = ({ +export const StartButton: FC> = ({ handleAction, }) => { const styles = useStyles() @@ -87,7 +88,7 @@ export const StartButton: FC> = ({ ) } -export const StopButton: FC> = ({ +export const StopButton: FC> = ({ handleAction, }) => { const styles = useStyles() @@ -103,7 +104,23 @@ export const StopButton: FC> = ({ ) } -export const DeleteButton: FC> = ({ +export const RestartButton: FC> = ({ + handleAction, +}) => { + const styles = useStyles() + const { t } = useTranslation("workspacePage") + + return ( + } + onClick={handleAction} + label={t("actionButton.restart")} + /> + ) +} + +export const DeleteButton: FC> = ({ handleAction, }) => { const styles = useStyles() @@ -119,7 +136,7 @@ export const DeleteButton: FC> = ({ ) } -export const CancelButton: FC> = ({ +export const CancelButton: FC> = ({ handleAction, }) => { const styles = useStyles() @@ -146,7 +163,7 @@ interface DisabledProps { label: string } -export const DisabledButton: FC> = ({ +export const DisabledButton: FC> = ({ label, }) => { const styles = useStyles() @@ -162,7 +179,7 @@ interface LoadingProps { label: string } -export const ActionLoadingButton: FC> = ({ +export const ActionLoadingButton: FC> = ({ label, }) => { const styles = useStyles() diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index ceb18f2483107..44c90a62abc44 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -43,6 +43,7 @@ export interface WorkspaceProps { } handleStart: () => void handleStop: () => void + handleRestart: () => void handleDelete: () => void handleUpdate: () => void handleCancel: () => void @@ -74,6 +75,7 @@ export const Workspace: FC> = ({ scheduleProps, handleStart, handleStop, + handleRestart, handleDelete, handleUpdate, handleCancel, @@ -142,6 +144,7 @@ export const Workspace: FC> = ({ isOutdated={workspace.outdated} handleStart={handleStart} handleStop={handleStop} + handleRestart={handleRestart} handleDelete={handleDelete} handleUpdate={handleUpdate} handleCancel={handleCancel} diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.stories.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.stories.tsx index 090e81cb2bb03..b2b2526811d0d 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.stories.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.stories.tsx @@ -15,6 +15,7 @@ const Template: Story = (args) => ( const defaultArgs = { handleStart: action("start"), handleStop: action("stop"), + handleRestart: action("restart"), handleDelete: action("delete"), handleUpdate: action("update"), handleCancel: action("cancel"), diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx index 98c8c3e7fd1af..a90ecaac93a07 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx @@ -15,6 +15,7 @@ const renderComponent = async (props: Partial = {}) => { isOutdated={props.isOutdated ?? false} handleStart={jest.fn()} handleStop={jest.fn()} + handleRestart={jest.fn()} handleDelete={jest.fn()} handleUpdate={jest.fn()} handleCancel={jest.fn()} @@ -33,6 +34,7 @@ const renderAndClick = async (props: Partial = {}) => { isOutdated={props.isOutdated ?? false} handleStart={jest.fn()} handleStop={jest.fn()} + handleRestart={jest.fn()} handleDelete={jest.fn()} handleUpdate={jest.fn()} handleCancel={jest.fn()} @@ -62,7 +64,7 @@ describe("WorkspaceActions", () => { }) }) describe("when the workspace is started", () => { - it("primary is stop; secondary is delete", async () => { + it("primary is stop; secondary are delete, restart", async () => { await renderAndClick({ workspaceStatus: Mocks.MockWorkspace.latest_build.status, }) @@ -72,6 +74,9 @@ describe("WorkspaceActions", () => { expect(screen.getByTestId("secondary-ctas")).toHaveTextContent( t("actionButton.delete", { ns: "workspacePage" }), ) + expect(screen.getByTestId("secondary-ctas")).toHaveTextContent( + t("actionButton.restart", { ns: "workspacePage" }), + ) }) }) describe("when the workspace is started", () => { diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.tsx index 7a1c4e3aba14d..58cf83b756888 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.tsx @@ -10,6 +10,7 @@ import { SettingsButton, StartButton, StopButton, + RestartButton, UpdateButton, } from "../DropdownButton/ActionCtas" import { ButtonMapping, ButtonTypesEnum, buttonAbilities } from "./constants" @@ -19,6 +20,7 @@ export interface WorkspaceActionsProps { isOutdated: boolean handleStart: () => void handleStop: () => void + handleRestart: () => void handleDelete: () => void handleUpdate: () => void handleCancel: () => void @@ -34,6 +36,7 @@ export const WorkspaceActions: FC = ({ isOutdated, handleStart, handleStop, + handleRestart, handleDelete, handleUpdate, handleCancel, @@ -68,6 +71,10 @@ export const WorkspaceActions: FC = ({ [ButtonTypesEnum.stopping]: ( ), + [ButtonTypesEnum.restart]: , + [ButtonTypesEnum.starting]: ( + + ), [ButtonTypesEnum.delete]: , [ButtonTypesEnum.deleting]: ( diff --git a/site/src/components/WorkspaceActions/constants.ts b/site/src/components/WorkspaceActions/constants.ts index ada352871f2c3..44b8b10e8835f 100644 --- a/site/src/components/WorkspaceActions/constants.ts +++ b/site/src/components/WorkspaceActions/constants.ts @@ -7,6 +7,7 @@ export enum ButtonTypesEnum { starting = "starting", stop = "stop", stopping = "stopping", + restart = "restart", delete = "delete", deleting = "deleting", update = "update", @@ -47,6 +48,7 @@ const statusToAbilities: Record = { ButtonTypesEnum.settings, ButtonTypesEnum.changeVersion, ButtonTypesEnum.delete, + ButtonTypesEnum.restart, ], canCancel: false, canAcceptJobs: true, diff --git a/site/src/i18n/en/workspacePage.json b/site/src/i18n/en/workspacePage.json index fa5e9f8335021..fdcfc6ea31a65 100644 --- a/site/src/i18n/en/workspacePage.json +++ b/site/src/i18n/en/workspacePage.json @@ -21,6 +21,7 @@ "actionButton": { "start": "Start", "stop": "Stop", + "restart": "Restart", "delete": "Delete", "cancel": "Cancel", "update": "Update", diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index c688735974787..987e6ca9eb894 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -124,6 +124,7 @@ export const WorkspaceReadyPage = ({ workspace={workspace} handleStart={() => workspaceSend({ type: "START" })} handleStop={() => workspaceSend({ type: "STOP" })} + handleRestart={() => workspaceSend({ type: "RESTART" })} handleDelete={() => workspaceSend({ type: "ASK_DELETE" })} handleUpdate={() => workspaceSend({ type: "UPDATE" })} handleCancel={() => workspaceSend({ type: "CANCEL" })} diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index 2188f33d70949..10690d9746c36 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -88,6 +88,7 @@ export type WorkspaceEvent = | { type: "REFRESH_WORKSPACE"; data: TypesGen.ServerSentEvent["data"] } | { type: "START" } | { type: "STOP" } + | { type: "RESTART" } | { type: "ASK_DELETE" } | { type: "DELETE" } | { type: "CANCEL_DELETE" } @@ -173,6 +174,9 @@ export const workspaceMachine = createMachine( stopWorkspace: { data: TypesGen.WorkspaceBuild } + restartWorkspace: { + data: TypesGen.WorkspaceBuild + } deleteWorkspace: { data: TypesGen.WorkspaceBuild } @@ -298,6 +302,7 @@ export const workspaceMachine = createMachine( on: { START: "requestingStart", STOP: "requestingStop", + RESTART: "requestingRestart", ASK_DELETE: "askingDelete", UPDATE: "requestingUpdate", CHANGE_VERSION: { @@ -426,6 +431,25 @@ export const workspaceMachine = createMachine( ], }, }, + requestingRestart: { + entry: ["clearBuildError", "updateStatusToPending"], + invoke: { + src: "restartWorkspace", + id: "restartWorkspace", + onDone: [ + { + actions: ["assignBuild", "disableDebugMode"], + target: "idle", + }, + ], + onError: [ + { + actions: "assignBuildError", + target: "idle", + }, + ], + }, + }, requestingDelete: { entry: ["clearBuildError", "updateStatusToPending"], invoke: { @@ -801,6 +825,17 @@ export const workspaceMachine = createMachine( throw Error("Cannot stop workspace without workspace id") } }, + restartWorkspace: (context) => async (send) => { + if (context.workspace) { + const restartWorkspacePromise = await API.restartWorkspace( + context.workspace.id, + ) + send({ type: "REFRESH_TIMELINE" }) + return restartWorkspacePromise + } else { + throw Error("Cannot restart workspace without workspace id") + } + }, deleteWorkspace: async (context) => { if (context.workspace) { const deleteWorkspacePromise = await API.deleteWorkspace(