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", pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy