Skip to content

Commit dad033e

Browse files
fix(site): exclude workspace schedule settings for prebuilt workspaces (#18826)
## Description This PR updates the UI to avoid rendering workspace schedule settings (autostop, autostart, etc.) for prebuilt workspaces. Instead, it displays an informational message with a link to the relevant documentation. ## Changes * Introduce `IsPrebuild` parameter to `convertWorkspace` to indicate whether the workspace is a prebuild. * Prevent the Workspace Schedule settings form from rendering in the UI for prebuilt workspaces. * Display an info alert with a link to documentation when viewing a prebuilt workspace. <img width="2980" height="864" alt="Screenshot 2025-07-10 at 13 16 13" src="https://github.com/user-attachments/assets/5f831c21-50bb-4e05-beea-dbeb930ddff8" /> Relates with: #18762 --------- Co-authored-by: BrunoQuaresma <bruno_nonato_quaresma@hotmail.com>
1 parent e4d3453 commit dad033e

File tree

11 files changed

+220
-70
lines changed

11 files changed

+220
-70
lines changed

cli/testdata/coder_list_--output_json.golden

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
"automatic_updates": "never",
8787
"allow_renames": false,
8888
"favorite": false,
89-
"next_start_at": "====[timestamp]====="
89+
"next_start_at": "====[timestamp]=====",
90+
"is_prebuild": false
9091
}
9192
]

coderd/apidoc/docs.go

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/workspaces.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2231,6 +2231,7 @@ func convertWorkspace(
22312231
if latestAppStatus.ID == uuid.Nil {
22322232
appStatus = nil
22332233
}
2234+
22342235
return codersdk.Workspace{
22352236
ID: workspace.ID,
22362237
CreatedAt: workspace.CreatedAt,
@@ -2265,6 +2266,7 @@ func convertWorkspace(
22652266
AllowRenames: allowRenames,
22662267
Favorite: requesterFavorite,
22672268
NextStartAt: nextStartAt,
2269+
IsPrebuild: workspace.IsPrebuild(),
22682270
}, nil
22692271
}
22702272

codersdk/workspaces.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ type Workspace struct {
6666
AllowRenames bool `json:"allow_renames"`
6767
Favorite bool `json:"favorite"`
6868
NextStartAt *time.Time `json:"next_start_at" format:"date-time"`
69+
// IsPrebuild indicates whether the workspace is a prebuilt workspace.
70+
// Prebuilt workspaces are owned by the prebuilds system user and have specific behavior,
71+
// such as being managed differently from regular workspaces.
72+
// Once a prebuilt workspace is claimed by a user, it transitions to a regular workspace,
73+
// and IsPrebuild returns false.
74+
IsPrebuild bool `json:"is_prebuild"`
6975
}
7076

7177
func (w Workspace) FullName() string {

docs/reference/api/schemas.md

Lines changed: 35 additions & 32 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/reference/api/workspaces.md

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

site/src/api/typesGenerated.ts

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { getAuthorizationKey } from "api/queries/authCheck";
3+
import { templateByNameKey } from "api/queries/templates";
4+
import { workspaceByOwnerAndNameKey } from "api/queries/workspaces";
5+
import type { Workspace } from "api/typesGenerated";
6+
import {
7+
reactRouterNestedAncestors,
8+
reactRouterParameters,
9+
} from "storybook-addon-remix-react-router";
10+
import {
11+
MockPrebuiltWorkspace,
12+
MockTemplate,
13+
MockUserOwner,
14+
MockWorkspace,
15+
} from "testHelpers/entities";
16+
import { withAuthProvider, withDashboardProvider } from "testHelpers/storybook";
17+
import { WorkspaceSettingsLayout } from "../WorkspaceSettingsLayout";
18+
import WorkspaceSchedulePage from "./WorkspaceSchedulePage";
19+
20+
const meta = {
21+
title: "pages/WorkspaceSchedulePage",
22+
component: WorkspaceSchedulePage,
23+
decorators: [withAuthProvider, withDashboardProvider],
24+
parameters: {
25+
layout: "fullscreen",
26+
user: MockUserOwner,
27+
},
28+
} satisfies Meta<typeof WorkspaceSchedulePage>;
29+
30+
export default meta;
31+
type Story = StoryObj<typeof WorkspaceSchedulePage>;
32+
33+
export const RegularWorkspace: Story = {
34+
parameters: {
35+
reactRouter: workspaceRouterParameters(MockWorkspace),
36+
queries: workspaceQueries(MockWorkspace),
37+
},
38+
};
39+
40+
export const PrebuiltWorkspace: Story = {
41+
parameters: {
42+
reactRouter: workspaceRouterParameters(MockPrebuiltWorkspace),
43+
queries: workspaceQueries(MockPrebuiltWorkspace),
44+
},
45+
};
46+
47+
function workspaceRouterParameters(workspace: Workspace) {
48+
return reactRouterParameters({
49+
location: {
50+
pathParams: {
51+
username: `@${workspace.owner_name}`,
52+
workspace: workspace.name,
53+
},
54+
},
55+
routing: reactRouterNestedAncestors(
56+
{
57+
path: "/:username/:workspace/settings/schedule",
58+
},
59+
<WorkspaceSettingsLayout />,
60+
),
61+
});
62+
}
63+
64+
function workspaceQueries(workspace: Workspace) {
65+
return [
66+
{
67+
key: workspaceByOwnerAndNameKey(workspace.owner_name, workspace.name),
68+
data: workspace,
69+
},
70+
{
71+
key: getAuthorizationKey({
72+
checks: {
73+
updateWorkspace: {
74+
object: {
75+
resource_type: "workspace",
76+
resource_id: MockWorkspace.id,
77+
owner_id: MockWorkspace.owner_id,
78+
},
79+
action: "update",
80+
},
81+
},
82+
}),
83+
data: { updateWorkspace: true },
84+
},
85+
{
86+
key: templateByNameKey(
87+
MockWorkspace.organization_id,
88+
MockWorkspace.template_name,
89+
),
90+
data: MockTemplate,
91+
},
92+
];
93+
}

site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx

Lines changed: 59 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Alert } from "components/Alert/Alert";
77
import { ErrorAlert } from "components/Alert/ErrorAlert";
88
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
99
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
10+
import { Link } from "components/Link/Link";
1011
import { Loader } from "components/Loader/Loader";
1112
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader";
1213
import dayjs from "dayjs";
@@ -20,6 +21,7 @@ import { type FC, useState } from "react";
2021
import { Helmet } from "react-helmet-async";
2122
import { useMutation, useQuery, useQueryClient } from "react-query";
2223
import { useNavigate, useParams } from "react-router-dom";
24+
import { docs } from "utils/docs";
2325
import { pageTitle } from "utils/page";
2426
import { WorkspaceScheduleForm } from "./WorkspaceScheduleForm";
2527
import {
@@ -32,7 +34,7 @@ const permissionsToCheck = (workspace: TypesGen.Workspace) =>
3234
updateWorkspace: {
3335
object: {
3436
resource_type: "workspace",
35-
resourceId: workspace.id,
37+
resource_id: workspace.id,
3638
owner_id: workspace.owner_id,
3739
},
3840
action: "update",
@@ -94,42 +96,62 @@ const WorkspaceSchedulePage: FC = () => {
9496
</Alert>
9597
)}
9698

97-
{template && (
98-
<WorkspaceScheduleForm
99-
template={template}
100-
error={submitScheduleMutation.error}
101-
initialValues={{
102-
...getAutostart(workspace),
103-
...getAutostop(workspace),
104-
}}
105-
isLoading={submitScheduleMutation.isPending}
106-
defaultTTL={dayjs.duration(template.default_ttl_ms, "ms").asHours()}
107-
onCancel={() => {
108-
navigate(`/@${username}/${workspaceName}`);
109-
}}
110-
onSubmit={async (values) => {
111-
const data = {
112-
workspace,
113-
autostart: formValuesToAutostartRequest(values),
114-
ttl: formValuesToTTLRequest(values),
115-
autostartChanged: scheduleChanged(
116-
getAutostart(workspace),
117-
values,
118-
),
119-
autostopChanged: scheduleChanged(getAutostop(workspace), values),
120-
};
121-
122-
await submitScheduleMutation.mutateAsync(data);
123-
124-
if (
125-
data.autostopChanged &&
126-
getAutostop(workspace).autostopEnabled
127-
) {
128-
setIsConfirmingApply(true);
129-
}
130-
}}
131-
/>
132-
)}
99+
{template &&
100+
(workspace.is_prebuild ? (
101+
<Alert severity="info">
102+
Prebuilt workspaces ignore workspace-level scheduling until they are
103+
claimed. For prebuilt workspace specific scheduling refer to the{" "}
104+
<Link
105+
title="Prebuilt Workspaces Scheduling"
106+
href={docs(
107+
"/admin/templates/extending-templates/prebuilt-workspaces#scheduling",
108+
)}
109+
target="_blank"
110+
rel="noreferrer"
111+
>
112+
Prebuilt Workspaces Scheduling
113+
</Link>
114+
documentation page.
115+
</Alert>
116+
) : (
117+
<WorkspaceScheduleForm
118+
template={template}
119+
error={submitScheduleMutation.error}
120+
initialValues={{
121+
...getAutostart(workspace),
122+
...getAutostop(workspace),
123+
}}
124+
isLoading={submitScheduleMutation.isPending}
125+
defaultTTL={dayjs.duration(template.default_ttl_ms, "ms").asHours()}
126+
onCancel={() => {
127+
navigate(`/@${username}/${workspaceName}`);
128+
}}
129+
onSubmit={async (values) => {
130+
const data = {
131+
workspace,
132+
autostart: formValuesToAutostartRequest(values),
133+
ttl: formValuesToTTLRequest(values),
134+
autostartChanged: scheduleChanged(
135+
getAutostart(workspace),
136+
values,
137+
),
138+
autostopChanged: scheduleChanged(
139+
getAutostop(workspace),
140+
values,
141+
),
142+
};
143+
144+
await submitScheduleMutation.mutateAsync(data);
145+
146+
if (
147+
data.autostopChanged &&
148+
getAutostop(workspace).autostopEnabled
149+
) {
150+
setIsConfirmingApply(true);
151+
}
152+
}}
153+
/>
154+
))}
133155

134156
<ConfirmDialog
135157
open={isConfirmingApply}

0 commit comments

Comments
 (0)