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 9ca9c6ed64350..7a3bd8a0d913a 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -17653,6 +17653,10 @@ const docTemplate = `{ "type": "string", "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": { "type": "string", "format": "date-time" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index e47798c1629fd..ded07f40f1163 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -16116,6 +16116,10 @@ "type": "string", "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": { "type": "string", "format": "date-time" diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 05eae8f5145e6..32b412946907e 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -2231,6 +2231,7 @@ func convertWorkspace( if latestAppStatus.ID == uuid.Nil { appStatus = nil } + return codersdk.Workspace{ ID: workspace.ID, CreatedAt: workspace.CreatedAt, @@ -2265,6 +2266,7 @@ func convertWorkspace( AllowRenames: allowRenames, Favorite: requesterFavorite, NextStartAt: nextStartAt, + IsPrebuild: workspace.IsPrebuild(), }, nil } diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index c776f2cf5a473..871a9d5b3fd31 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -66,6 +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 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 { diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index d0bbc2c079daa..053a738413060 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -8667,6 +8667,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", @@ -8906,38 +8907,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 | | | -| `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 @@ -10505,6 +10507,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 0b6148f796f6b..47a2984d374a2 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -3448,6 +3448,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/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.stories.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.stories.tsx new file mode 100644 index 0000000000000..e576e479d27c7 --- /dev/null +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.stories.tsx @@ -0,0 +1,93 @@ +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, +} from "storybook-addon-remix-react-router"; +import { + MockPrebuiltWorkspace, + MockTemplate, + MockUserOwner, + MockWorkspace, +} from "testHelpers/entities"; +import { withAuthProvider, withDashboardProvider } from "testHelpers/storybook"; +import { WorkspaceSettingsLayout } from "../WorkspaceSettingsLayout"; +import WorkspaceSchedulePage from "./WorkspaceSchedulePage"; + +const meta = { + title: "pages/WorkspaceSchedulePage", + component: WorkspaceSchedulePage, + decorators: [withAuthProvider, withDashboardProvider], + parameters: { + layout: "fullscreen", + user: MockUserOwner, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const RegularWorkspace: Story = { + parameters: { + reactRouter: workspaceRouterParameters(MockWorkspace), + queries: workspaceQueries(MockWorkspace), + }, +}; + +export const PrebuiltWorkspace: Story = { + parameters: { + 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/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx index 597b20173fafa..4c8526a4cda6b 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx @@ -7,6 +7,7 @@ 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"; @@ -20,6 +21,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 { @@ -32,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", @@ -94,42 +96,62 @@ 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 && + (workspace.is_prebuild ? ( + + Prebuilt workspaces ignore workspace-level scheduling until they are + claimed. 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); + } + }} + /> + ))} 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