From 95ca5f70a34ec15d93ffbb0db00255a99bbb4d88 Mon Sep 17 00:00:00 2001 From: Susana Ferreira Date: Thu, 10 Jul 2025 12:03:19 +0000 Subject: [PATCH 1/9] fix(site): exclude workspace schedule settings for prebuilt workspaces --- site/src/modules/workspaces/prebuilds.ts | 7 ++ .../WorkspaceSchedulePage.tsx | 98 ++++++++++++------- 2 files changed, 69 insertions(+), 36 deletions(-) create mode 100644 site/src/modules/workspaces/prebuilds.ts diff --git a/site/src/modules/workspaces/prebuilds.ts b/site/src/modules/workspaces/prebuilds.ts new file mode 100644 index 0000000000000..74553565756d4 --- /dev/null +++ b/site/src/modules/workspaces/prebuilds.ts @@ -0,0 +1,7 @@ +import type { Workspace } from "api/typesGenerated"; + +// Returns true if the workspace is a prebuilt workspace (owned by the prebuilds system user), +// otherwise returns false. +export const isPrebuiltWorkspace = (workspace: Workspace): boolean => { + return workspace.owner_id === "c42fdf75-3097-471c-8c33-fb52454d81c0"; +}; diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx index 597b20173fafa..3a82544d31ee9 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx @@ -7,9 +7,11 @@ import { Alert } from "components/Alert/Alert"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"; +import { Link } from "components/Link/Link"; import { Loader } from "components/Loader/Loader"; import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; import dayjs from "dayjs"; +import { isPrebuiltWorkspace } from "modules/workspaces/prebuilds"; import { scheduleChanged, scheduleToAutostart, @@ -20,6 +22,7 @@ import { type FC, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQuery, useQueryClient } from "react-query"; import { useNavigate, useParams } from "react-router-dom"; +import { docs } from "utils/docs"; import { pageTitle } from "utils/page"; import { WorkspaceScheduleForm } from "./WorkspaceScheduleForm"; import { @@ -94,42 +97,65 @@ const WorkspaceSchedulePage: FC = () => { )} - {template && ( - { - 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 && - getAutostop(workspace).autostopEnabled - ) { - setIsConfirmingApply(true); - } - }} - /> - )} + {template && + // Prebuilt workspaces have their own scheduling system, + // so we avoid showing the workspace-level schedule settings form. + // Instead, show an informational message with a link to the relevant docs. + (isPrebuiltWorkspace(workspace) ? ( + + Prebuilt workspaces do not support workspace-level scheduling. For + prebuilt workspace specific scheduling refer to the{" "} + + Prebuilt Workspaces Scheduling + + documentation page. + + ) : ( + { + 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 && + getAutostop(workspace).autostopEnabled + ) { + setIsConfirmingApply(true); + } + }} + /> + ))} Date: Thu, 10 Jul 2025 14:48:10 +0000 Subject: [PATCH 2/9] feat: add is_prebuild flag to workspace API response --- cli/testdata/coder_list_--output_json.golden | 3 ++- coderd/apidoc/docs.go | 3 +++ coderd/apidoc/swagger.json | 3 +++ coderd/workspaces.go | 7 +++++++ codersdk/workspaces.go | 1 + docs/reference/api/schemas.md | 3 +++ docs/reference/api/workspaces.md | 6 ++++++ site/src/api/typesGenerated.ts | 1 + site/src/modules/workspaces/prebuilds.ts | 7 ------- .../WorkspaceSchedulePage/WorkspaceSchedulePage.tsx | 10 +++------- site/src/testHelpers/entities.ts | 1 + 11 files changed, 30 insertions(+), 15 deletions(-) delete mode 100644 site/src/modules/workspaces/prebuilds.ts diff --git a/cli/testdata/coder_list_--output_json.golden b/cli/testdata/coder_list_--output_json.golden index e97894c4afb21..51c2887cd1e4a 100644 --- a/cli/testdata/coder_list_--output_json.golden +++ b/cli/testdata/coder_list_--output_json.golden @@ -86,6 +86,7 @@ "automatic_updates": "never", "allow_renames": false, "favorite": false, - "next_start_at": "====[timestamp]=====" + "next_start_at": "====[timestamp]=====", + "is_prebuild": false } ] diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 79cff80b1fbc5..6138c348f13e5 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -17437,6 +17437,9 @@ const docTemplate = `{ "type": "string", "format": "uuid" }, + "is_prebuild": { + "type": "boolean" + }, "last_used_at": { "type": "string", "format": "date-time" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 5fa1d98030cb5..7e13cde2fdd2d 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -15908,6 +15908,9 @@ "type": "string", "format": "uuid" }, + "is_prebuild": { + "type": "boolean" + }, "last_used_at": { "type": "string", "format": "date-time" diff --git a/coderd/workspaces.go b/coderd/workspaces.go index ecb624d1bc09f..9a9cfc2c82ce5 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -2231,6 +2231,12 @@ func convertWorkspace( if latestAppStatus.ID == uuid.Nil { appStatus = nil } + + isPrebuild := false + if workspace.OwnerID == database.PrebuildsSystemUserID { + isPrebuild = true + } + return codersdk.Workspace{ ID: workspace.ID, CreatedAt: workspace.CreatedAt, @@ -2265,6 +2271,7 @@ func convertWorkspace( AllowRenames: allowRenames, Favorite: requesterFavorite, NextStartAt: nextStartAt, + IsPrebuild: isPrebuild, }, nil } diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index c776f2cf5a473..4673a343fa4c0 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -66,6 +66,7 @@ type Workspace struct { AllowRenames bool `json:"allow_renames"` Favorite bool `json:"favorite"` NextStartAt *time.Time `json:"next_start_at" format:"date-time"` + IsPrebuild bool `json:"is_prebuild"` } func (w Workspace) FullName() string { diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 6ca1cfb9dfe51..b063cc18cdf2c 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -8444,6 +8444,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "healthy": false }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "is_prebuild": true, "last_used_at": "2019-08-24T14:15:22Z", "latest_app_status": { "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", @@ -8694,6 +8695,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| | `favorite` | boolean | false | | | | `health` | [codersdk.WorkspaceHealth](#codersdkworkspacehealth) | false | | Health shows the health of the workspace and information about what is causing an unhealthy status. | | `id` | string | false | | | +| `is_prebuild` | boolean | false | | | | `last_used_at` | string | false | | | | `latest_app_status` | [codersdk.WorkspaceAppStatus](#codersdkworkspaceappstatus) | false | | | | `latest_build` | [codersdk.WorkspaceBuild](#codersdkworkspacebuild) | false | | | @@ -10282,6 +10284,7 @@ If the schedule is empty, the user will be updated to use the default schedule.| "healthy": false }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "is_prebuild": true, "last_used_at": "2019-08-24T14:15:22Z", "latest_app_status": { "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", diff --git a/docs/reference/api/workspaces.md b/docs/reference/api/workspaces.md index a43a5f2c8fe18..debcb421e02e3 100644 --- a/docs/reference/api/workspaces.md +++ b/docs/reference/api/workspaces.md @@ -67,6 +67,7 @@ of the template will be used. "healthy": false }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "is_prebuild": true, "last_used_at": "2019-08-24T14:15:22Z", "latest_app_status": { "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", @@ -353,6 +354,7 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/workspace/{workspacenam "healthy": false }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "is_prebuild": true, "last_used_at": "2019-08-24T14:15:22Z", "latest_app_status": { "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", @@ -664,6 +666,7 @@ of the template will be used. "healthy": false }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "is_prebuild": true, "last_used_at": "2019-08-24T14:15:22Z", "latest_app_status": { "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", @@ -953,6 +956,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces \ "healthy": false }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "is_prebuild": true, "last_used_at": "2019-08-24T14:15:22Z", "latest_app_status": { "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", @@ -1223,6 +1227,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace} \ "healthy": false }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "is_prebuild": true, "last_used_at": "2019-08-24T14:15:22Z", "latest_app_status": { "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", @@ -1625,6 +1630,7 @@ curl -X PUT http://coder-server:8080/api/v2/workspaces/{workspace}/dormant \ "healthy": false }, "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "is_prebuild": true, "last_used_at": "2019-08-24T14:15:22Z", "latest_app_status": { "agent_id": "2b1e3b65-2c04-4fa2-a2d7-467901e98978", diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 53dc919df2df3..3f675c62b61cf 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -3375,6 +3375,7 @@ export interface Workspace { readonly allow_renames: boolean; readonly favorite: boolean; readonly next_start_at: string | null; + readonly is_prebuild: boolean; } // From codersdk/workspaceagents.go diff --git a/site/src/modules/workspaces/prebuilds.ts b/site/src/modules/workspaces/prebuilds.ts deleted file mode 100644 index 74553565756d4..0000000000000 --- a/site/src/modules/workspaces/prebuilds.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Workspace } from "api/typesGenerated"; - -// Returns true if the workspace is a prebuilt workspace (owned by the prebuilds system user), -// otherwise returns false. -export const isPrebuiltWorkspace = (workspace: Workspace): boolean => { - return workspace.owner_id === "c42fdf75-3097-471c-8c33-fb52454d81c0"; -}; diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx index 3a82544d31ee9..97b8a102cf39a 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx @@ -11,7 +11,6 @@ import { Link } from "components/Link/Link"; import { Loader } from "components/Loader/Loader"; import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; import dayjs from "dayjs"; -import { isPrebuiltWorkspace } from "modules/workspaces/prebuilds"; import { scheduleChanged, scheduleToAutostart, @@ -98,13 +97,10 @@ const WorkspaceSchedulePage: FC = () => { )} {template && - // Prebuilt workspaces have their own scheduling system, - // so we avoid showing the workspace-level schedule settings form. - // Instead, show an informational message with a link to the relevant docs. - (isPrebuiltWorkspace(workspace) ? ( + (workspace.is_prebuild ? ( - Prebuilt workspaces do not support workspace-level scheduling. For - prebuilt workspace specific scheduling refer to the{" "} + Prebuilt workspaces ignore workspace-level scheduling until they are claimed. + For prebuilt workspace specific scheduling refer to the{" "} Date: Thu, 10 Jul 2025 14:56:37 +0000 Subject: [PATCH 3/9] fix: fmt --- .../WorkspaceSchedulePage/WorkspaceSchedulePage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx index 97b8a102cf39a..1f4c334e602cf 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx @@ -99,8 +99,8 @@ const WorkspaceSchedulePage: FC = () => { {template && (workspace.is_prebuild ? ( - Prebuilt workspaces ignore workspace-level scheduling until they are claimed. - For prebuilt workspace specific scheduling refer to the{" "} + Prebuilt workspaces ignore workspace-level scheduling until they are + claimed. For prebuilt workspace specific scheduling refer to the{" "} Date: Mon, 14 Jul 2025 16:18:28 +0000 Subject: [PATCH 4/9] chore: add comment to IsPrebuild parameter on codersdk.Workspaces --- codersdk/workspaces.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 4673a343fa4c0..871a9d5b3fd31 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -66,7 +66,12 @@ type Workspace struct { AllowRenames bool `json:"allow_renames"` Favorite bool `json:"favorite"` NextStartAt *time.Time `json:"next_start_at" format:"date-time"` - IsPrebuild bool `json:"is_prebuild"` + // IsPrebuild indicates whether the workspace is a prebuilt workspace. + // Prebuilt workspaces are owned by the prebuilds system user and have specific behavior, + // such as being managed differently from regular workspaces. + // Once a prebuilt workspace is claimed by a user, it transitions to a regular workspace, + // and IsPrebuild returns false. + IsPrebuild bool `json:"is_prebuild"` } func (w Workspace) FullName() string { From 73f54764b7c31567d56aee71a2fdaf815176de85 Mon Sep 17 00:00:00 2001 From: Susana Ferreira Date: Mon, 14 Jul 2025 16:20:17 +0000 Subject: [PATCH 5/9] chore: add story to WorkspaceSchedulePage --- .../WorkspaceSchedulePage.stories.tsx | 120 ++++++++++++++++++ .../WorkspaceSchedulePage.tsx | 2 +- .../WorkspaceSettingsLayout.tsx | 2 + site/src/testHelpers/entities.ts | 7 + 4 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.stories.tsx diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.stories.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.stories.tsx new file mode 100644 index 0000000000000..a895e279ed9d9 --- /dev/null +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.stories.tsx @@ -0,0 +1,120 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { getAuthorizationKey } from "api/queries/authCheck"; +import { templateByNameKey } from "api/queries/templates"; +import { workspaceByOwnerAndNameKey } from "api/queries/workspaces"; +import { AuthProvider } from "contexts/auth/AuthProvider"; +import { RequireAuth } from "contexts/auth/RequireAuth"; +import { permissionChecks } from "modules/permissions"; +import { + reactRouterOutlet, + reactRouterParameters, +} from "storybook-addon-remix-react-router"; +import { + MockAppearanceConfig, + MockAuthMethodsAll, + MockBuildInfo, + MockDefaultOrganization, + MockEntitlements, + MockExperiments, + MockPrebuiltWorkspace, + MockTemplate, + MockUserAppearanceSettings, + MockUserOwner, + MockWorkspace, +} from "testHelpers/entities"; +import WorkspaceSchedulePage from "./WorkspaceSchedulePage"; + +import { WorkspaceSettingsContext } from "../WorkspaceSettingsLayout"; + +const meta = { + title: "pages/WorkspaceSchedulePage", + component: RequireAuth, + parameters: { + layout: "fullscreen", + reactRouter: reactRouterParameters({ + location: { + pathParams: { + username: `@${MockWorkspace.owner_name}`, + workspace: MockWorkspace.name, + }, + }, + routing: reactRouterOutlet( + { + path: "/:username/:workspace/settings/schedule", + }, + , + ), + }), + queries: [ + { key: ["me"], data: MockUserOwner }, + { key: ["authMethods"], data: MockAuthMethodsAll }, + { key: ["hasFirstUser"], data: true }, + { key: ["buildInfo"], data: MockBuildInfo }, + { key: ["entitlements"], data: MockEntitlements }, + { key: ["experiments"], data: MockExperiments }, + { key: ["appearance"], data: MockAppearanceConfig }, + { key: ["organizations"], data: [MockDefaultOrganization] }, + { + key: getAuthorizationKey({ checks: permissionChecks }), + data: { editWorkspaceProxies: true }, + }, + { key: ["me", "appearance"], data: MockUserAppearanceSettings }, + { + key: workspaceByOwnerAndNameKey( + MockWorkspace.owner_name, + MockWorkspace.name, + ), + data: MockWorkspace, + }, + { + key: getAuthorizationKey({ + checks: { + updateWorkspace: { + object: { + resource_type: "workspace", + resource_id: MockWorkspace.id, + owner_id: MockWorkspace.owner_id, + }, + action: "update", + }, + }, + }), + data: { updateWorkspace: true }, + }, + { + key: templateByNameKey( + MockWorkspace.organization_id, + MockWorkspace.template_name, + ), + data: MockTemplate, + }, + ], + }, + decorators: [ + (Story, { parameters }) => { + const workspace = parameters.workspace || MockWorkspace; + return ( + + + + + + ); + }, + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const RegularWorkspace: Story = { + parameters: { + workspace: MockWorkspace, + }, +}; + +export const PrebuiltWorkspace: Story = { + parameters: { + workspace: MockPrebuiltWorkspace, + }, +}; diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx index 1f4c334e602cf..4c8526a4cda6b 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx @@ -34,7 +34,7 @@ const permissionsToCheck = (workspace: TypesGen.Workspace) => updateWorkspace: { object: { resource_type: "workspace", - resourceId: workspace.id, + resource_id: workspace.id, owner_id: workspace.owner_id, }, action: "update", diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsLayout.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsLayout.tsx index f3a36c98475e4..20f83c197c622 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsLayout.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsLayout.tsx @@ -69,3 +69,5 @@ export const WorkspaceSettingsLayout: FC = () => { ); }; + +export const WorkspaceSettingsContext = WorkspaceSettings; diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 6599ef30a91e1..660e4c31ece26 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -1414,6 +1414,13 @@ export const MockWorkspace: TypesGen.Workspace = { is_prebuild: false, }; +export const MockPrebuiltWorkspace = { + ...MockWorkspace, + owner_name: "prebuilds", + name: "prebuilt-workspace", + is_prebuild: true, +}; + export const MockFavoriteWorkspace: TypesGen.Workspace = { ...MockWorkspace, id: "test-favorite-workspace", From d5e45bbad8eae1a2765b4236e0e135d713ca7127 Mon Sep 17 00:00:00 2001 From: Susana Ferreira Date: Mon, 14 Jul 2025 17:03:06 +0000 Subject: [PATCH 6/9] fix: run make gen --- coderd/apidoc/docs.go | 1 + coderd/apidoc/swagger.json | 1 + docs/reference/api/schemas.md | 66 +++++++++++++++++------------------ 3 files changed, 35 insertions(+), 33 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 6138c348f13e5..e91eb59dc322c 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -17438,6 +17438,7 @@ const docTemplate = `{ "format": "uuid" }, "is_prebuild": { + "description": "IsPrebuild indicates whether the workspace is a prebuilt workspace.\nPrebuilt workspaces are owned by the prebuilds system user and have specific behavior,\nsuch as being managed differently from regular workspaces.\nOnce a prebuilt workspace is claimed by a user, it transitions to a regular workspace,\nand IsPrebuild returns false.", "type": "boolean" }, "last_used_at": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 7e13cde2fdd2d..013a19a05f2a2 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -15909,6 +15909,7 @@ "format": "uuid" }, "is_prebuild": { + "description": "IsPrebuild indicates whether the workspace is a prebuilt workspace.\nPrebuilt workspaces are owned by the prebuilds system user and have specific behavior,\nsuch as being managed differently from regular workspaces.\nOnce a prebuilt workspace is claimed by a user, it transitions to a regular workspace,\nand IsPrebuild returns false.", "type": "boolean" }, "last_used_at": { diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index b063cc18cdf2c..9947ec15ec5f8 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -8684,39 +8684,39 @@ If the schedule is empty, the user will be updated to use the default schedule.| ### Properties -| Name | Type | Required | Restrictions | Description | -|---------------------------------------------|------------------------------------------------------------|----------|--------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `allow_renames` | boolean | false | | | -| `automatic_updates` | [codersdk.AutomaticUpdates](#codersdkautomaticupdates) | false | | | -| `autostart_schedule` | string | false | | | -| `created_at` | string | false | | | -| `deleting_at` | string | false | | Deleting at indicates the time at which the workspace will be permanently deleted. A workspace is eligible for deletion if it is dormant (a non-nil dormant_at value) and a value has been specified for time_til_dormant_autodelete on its template. | -| `dormant_at` | string | false | | Dormant at being non-nil indicates a workspace that is dormant. A dormant workspace is no longer accessible must be activated. It is subject to deletion if it breaches the duration of the time_til_ field on its template. | -| `favorite` | boolean | false | | | -| `health` | [codersdk.WorkspaceHealth](#codersdkworkspacehealth) | false | | Health shows the health of the workspace and information about what is causing an unhealthy status. | -| `id` | string | false | | | -| `is_prebuild` | boolean | false | | | -| `last_used_at` | string | false | | | -| `latest_app_status` | [codersdk.WorkspaceAppStatus](#codersdkworkspaceappstatus) | false | | | -| `latest_build` | [codersdk.WorkspaceBuild](#codersdkworkspacebuild) | false | | | -| `name` | string | false | | | -| `next_start_at` | string | false | | | -| `organization_id` | string | false | | | -| `organization_name` | string | false | | | -| `outdated` | boolean | false | | | -| `owner_avatar_url` | string | false | | | -| `owner_id` | string | false | | | -| `owner_name` | string | false | | Owner name is the username of the owner of the workspace. | -| `template_active_version_id` | string | false | | | -| `template_allow_user_cancel_workspace_jobs` | boolean | false | | | -| `template_display_name` | string | false | | | -| `template_icon` | string | false | | | -| `template_id` | string | false | | | -| `template_name` | string | false | | | -| `template_require_active_version` | boolean | false | | | -| `template_use_classic_parameter_flow` | boolean | false | | | -| `ttl_ms` | integer | false | | | -| `updated_at` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|---------------------------------------------|------------------------------------------------------------|----------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `allow_renames` | boolean | false | | | +| `automatic_updates` | [codersdk.AutomaticUpdates](#codersdkautomaticupdates) | false | | | +| `autostart_schedule` | string | false | | | +| `created_at` | string | false | | | +| `deleting_at` | string | false | | Deleting at indicates the time at which the workspace will be permanently deleted. A workspace is eligible for deletion if it is dormant (a non-nil dormant_at value) and a value has been specified for time_til_dormant_autodelete on its template. | +| `dormant_at` | string | false | | Dormant at being non-nil indicates a workspace that is dormant. A dormant workspace is no longer accessible must be activated. It is subject to deletion if it breaches the duration of the time_til_ field on its template. | +| `favorite` | boolean | false | | | +| `health` | [codersdk.WorkspaceHealth](#codersdkworkspacehealth) | false | | Health shows the health of the workspace and information about what is causing an unhealthy status. | +| `id` | string | false | | | +| `is_prebuild` | boolean | false | | Is prebuild indicates whether the workspace is a prebuilt workspace. Prebuilt workspaces are owned by the prebuilds system user and have specific behavior, such as being managed differently from regular workspaces. Once a prebuilt workspace is claimed by a user, it transitions to a regular workspace, and IsPrebuild returns false. | +| `last_used_at` | string | false | | | +| `latest_app_status` | [codersdk.WorkspaceAppStatus](#codersdkworkspaceappstatus) | false | | | +| `latest_build` | [codersdk.WorkspaceBuild](#codersdkworkspacebuild) | false | | | +| `name` | string | false | | | +| `next_start_at` | string | false | | | +| `organization_id` | string | false | | | +| `organization_name` | string | false | | | +| `outdated` | boolean | false | | | +| `owner_avatar_url` | string | false | | | +| `owner_id` | string | false | | | +| `owner_name` | string | false | | Owner name is the username of the owner of the workspace. | +| `template_active_version_id` | string | false | | | +| `template_allow_user_cancel_workspace_jobs` | boolean | false | | | +| `template_display_name` | string | false | | | +| `template_icon` | string | false | | | +| `template_id` | string | false | | | +| `template_name` | string | false | | | +| `template_require_active_version` | boolean | false | | | +| `template_use_classic_parameter_flow` | boolean | false | | | +| `ttl_ms` | integer | false | | | +| `updated_at` | string | false | | | #### Enumerated Values From 0120c5b41a7220cb343652ecb134cabdc8510094 Mon Sep 17 00:00:00 2001 From: Susana Ferreira Date: Mon, 14 Jul 2025 17:22:46 +0000 Subject: [PATCH 7/9] chore: use workspace.IsPrebuild() on convertWorkspace --- coderd/workspaces.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 9a9cfc2c82ce5..c75fde79c2948 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -2232,11 +2232,6 @@ func convertWorkspace( appStatus = nil } - isPrebuild := false - if workspace.OwnerID == database.PrebuildsSystemUserID { - isPrebuild = true - } - return codersdk.Workspace{ ID: workspace.ID, CreatedAt: workspace.CreatedAt, @@ -2271,7 +2266,7 @@ func convertWorkspace( AllowRenames: allowRenames, Favorite: requesterFavorite, NextStartAt: nextStartAt, - IsPrebuild: isPrebuild, + IsPrebuild: workspace.IsPrebuild(), }, nil } From ad475cf4f92a2adb67ca66a93cc44ca054992452 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 14 Jul 2025 18:50:47 +0000 Subject: [PATCH 8/9] simplify story --- .../WorkspaceSchedulePage.stories.tsx | 147 +++++++----------- .../WorkspaceSettingsLayout.tsx | 2 - 2 files changed, 60 insertions(+), 89 deletions(-) diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.stories.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.stories.tsx index a895e279ed9d9..b63ac9aa48cca 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.stories.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.stories.tsx @@ -1,107 +1,30 @@ import type { Meta, StoryObj } from "@storybook/react"; import { getAuthorizationKey } from "api/queries/authCheck"; import { templateByNameKey } from "api/queries/templates"; -import { workspaceByOwnerAndNameKey } from "api/queries/workspaces"; -import { AuthProvider } from "contexts/auth/AuthProvider"; -import { RequireAuth } from "contexts/auth/RequireAuth"; -import { permissionChecks } from "modules/permissions"; import { - reactRouterOutlet, + reactRouterNestedAncestors, reactRouterParameters, } from "storybook-addon-remix-react-router"; import { - MockAppearanceConfig, - MockAuthMethodsAll, - MockBuildInfo, - MockDefaultOrganization, - MockEntitlements, - MockExperiments, MockPrebuiltWorkspace, MockTemplate, - MockUserAppearanceSettings, MockUserOwner, MockWorkspace, } from "testHelpers/entities"; import WorkspaceSchedulePage from "./WorkspaceSchedulePage"; - -import { WorkspaceSettingsContext } from "../WorkspaceSettingsLayout"; +import { withAuthProvider, withDashboardProvider } from "testHelpers/storybook"; +import { WorkspaceSettingsLayout } from "../WorkspaceSettingsLayout"; +import { workspaceByOwnerAndNameKey } from "api/queries/workspaces"; +import type { Workspace } from "api/typesGenerated"; const meta = { title: "pages/WorkspaceSchedulePage", - component: RequireAuth, + component: WorkspaceSchedulePage, + decorators: [withAuthProvider, withDashboardProvider], parameters: { layout: "fullscreen", - reactRouter: reactRouterParameters({ - location: { - pathParams: { - username: `@${MockWorkspace.owner_name}`, - workspace: MockWorkspace.name, - }, - }, - routing: reactRouterOutlet( - { - path: "/:username/:workspace/settings/schedule", - }, - , - ), - }), - queries: [ - { key: ["me"], data: MockUserOwner }, - { key: ["authMethods"], data: MockAuthMethodsAll }, - { key: ["hasFirstUser"], data: true }, - { key: ["buildInfo"], data: MockBuildInfo }, - { key: ["entitlements"], data: MockEntitlements }, - { key: ["experiments"], data: MockExperiments }, - { key: ["appearance"], data: MockAppearanceConfig }, - { key: ["organizations"], data: [MockDefaultOrganization] }, - { - key: getAuthorizationKey({ checks: permissionChecks }), - data: { editWorkspaceProxies: true }, - }, - { key: ["me", "appearance"], data: MockUserAppearanceSettings }, - { - key: workspaceByOwnerAndNameKey( - MockWorkspace.owner_name, - MockWorkspace.name, - ), - data: MockWorkspace, - }, - { - key: getAuthorizationKey({ - checks: { - updateWorkspace: { - object: { - resource_type: "workspace", - resource_id: MockWorkspace.id, - owner_id: MockWorkspace.owner_id, - }, - action: "update", - }, - }, - }), - data: { updateWorkspace: true }, - }, - { - key: templateByNameKey( - MockWorkspace.organization_id, - MockWorkspace.template_name, - ), - data: MockTemplate, - }, - ], + user: MockUserOwner, }, - decorators: [ - (Story, { parameters }) => { - const workspace = parameters.workspace || MockWorkspace; - return ( - - - - - - ); - }, - ], } satisfies Meta; export default meta; @@ -109,12 +32,62 @@ type Story = StoryObj; export const RegularWorkspace: Story = { parameters: { - workspace: MockWorkspace, + reactRouter: workspaceRouterParameters(MockWorkspace), + queries: workspaceQueries(MockWorkspace), }, }; export const PrebuiltWorkspace: Story = { parameters: { - workspace: MockPrebuiltWorkspace, + reactRouter: workspaceRouterParameters(MockPrebuiltWorkspace), + queries: workspaceQueries(MockPrebuiltWorkspace), }, }; + +function workspaceRouterParameters(workspace: Workspace) { + return reactRouterParameters({ + location: { + pathParams: { + username: `@${workspace.owner_name}`, + workspace: workspace.name, + }, + }, + routing: reactRouterNestedAncestors( + { + path: "/:username/:workspace/settings/schedule", + }, + , + ), + }); +} + +function workspaceQueries(workspace: Workspace) { + return [ + { + key: workspaceByOwnerAndNameKey(workspace.owner_name, workspace.name), + data: workspace, + }, + { + key: getAuthorizationKey({ + checks: { + updateWorkspace: { + object: { + resource_type: "workspace", + resource_id: MockWorkspace.id, + owner_id: MockWorkspace.owner_id, + }, + action: "update", + }, + }, + }), + data: { updateWorkspace: true }, + }, + { + key: templateByNameKey( + MockWorkspace.organization_id, + MockWorkspace.template_name, + ), + data: MockTemplate, + }, + ]; +} diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsLayout.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsLayout.tsx index 20f83c197c622..f3a36c98475e4 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsLayout.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsLayout.tsx @@ -69,5 +69,3 @@ export const WorkspaceSettingsLayout: FC = () => { ); }; - -export const WorkspaceSettingsContext = WorkspaceSettings; From 02c0ff5ccacda6c40474587ee0f970b1b59e358a Mon Sep 17 00:00:00 2001 From: Susana Ferreira Date: Tue, 15 Jul 2025 09:40:57 +0000 Subject: [PATCH 9/9] fix: run make fmt --- .../WorkspaceSchedulePage/WorkspaceSchedulePage.stories.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.stories.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.stories.tsx index b63ac9aa48cca..e576e479d27c7 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.stories.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.stories.tsx @@ -1,6 +1,8 @@ import type { Meta, StoryObj } from "@storybook/react"; import { getAuthorizationKey } from "api/queries/authCheck"; import { templateByNameKey } from "api/queries/templates"; +import { workspaceByOwnerAndNameKey } from "api/queries/workspaces"; +import type { Workspace } from "api/typesGenerated"; import { reactRouterNestedAncestors, reactRouterParameters, @@ -11,11 +13,9 @@ import { MockUserOwner, MockWorkspace, } from "testHelpers/entities"; -import WorkspaceSchedulePage from "./WorkspaceSchedulePage"; import { withAuthProvider, withDashboardProvider } from "testHelpers/storybook"; import { WorkspaceSettingsLayout } from "../WorkspaceSettingsLayout"; -import { workspaceByOwnerAndNameKey } from "api/queries/workspaces"; -import type { Workspace } from "api/typesGenerated"; +import WorkspaceSchedulePage from "./WorkspaceSchedulePage"; const meta = { title: "pages/WorkspaceSchedulePage",