mirror of
https://github.com/coder/coder.git
synced 2025-07-21 01:28:49 +00:00
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: https://github.com/coder/coder/pull/18762 --------- Co-authored-by: BrunoQuaresma <bruno_nonato_quaresma@hotmail.com>
This commit is contained in:
3
cli/testdata/coder_list_--output_json.golden
vendored
3
cli/testdata/coder_list_--output_json.golden
vendored
@ -86,6 +86,7 @@
|
||||
"automatic_updates": "never",
|
||||
"allow_renames": false,
|
||||
"favorite": false,
|
||||
"next_start_at": "====[timestamp]====="
|
||||
"next_start_at": "====[timestamp]=====",
|
||||
"is_prebuild": false
|
||||
}
|
||||
]
|
||||
|
4
coderd/apidoc/docs.go
generated
4
coderd/apidoc/docs.go
generated
@ -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"
|
||||
|
4
coderd/apidoc/swagger.json
generated
4
coderd/apidoc/swagger.json
generated
@ -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"
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
67
docs/reference/api/schemas.md
generated
67
docs/reference/api/schemas.md
generated
@ -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",
|
||||
|
6
docs/reference/api/workspaces.md
generated
6
docs/reference/api/workspaces.md
generated
@ -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",
|
||||
|
1
site/src/api/typesGenerated.ts
generated
1
site/src/api/typesGenerated.ts
generated
@ -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
|
||||
|
@ -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<typeof WorkspaceSchedulePage>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof WorkspaceSchedulePage>;
|
||||
|
||||
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",
|
||||
},
|
||||
<WorkspaceSettingsLayout />,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
];
|
||||
}
|
@ -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 = () => {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{template && (
|
||||
<WorkspaceScheduleForm
|
||||
template={template}
|
||||
error={submitScheduleMutation.error}
|
||||
initialValues={{
|
||||
...getAutostart(workspace),
|
||||
...getAutostop(workspace),
|
||||
}}
|
||||
isLoading={submitScheduleMutation.isPending}
|
||||
defaultTTL={dayjs.duration(template.default_ttl_ms, "ms").asHours()}
|
||||
onCancel={() => {
|
||||
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),
|
||||
};
|
||||
{template &&
|
||||
(workspace.is_prebuild ? (
|
||||
<Alert severity="info">
|
||||
Prebuilt workspaces ignore workspace-level scheduling until they are
|
||||
claimed. For prebuilt workspace specific scheduling refer to the{" "}
|
||||
<Link
|
||||
title="Prebuilt Workspaces Scheduling"
|
||||
href={docs(
|
||||
"/admin/templates/extending-templates/prebuilt-workspaces#scheduling",
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Prebuilt Workspaces Scheduling
|
||||
</Link>
|
||||
documentation page.
|
||||
</Alert>
|
||||
) : (
|
||||
<WorkspaceScheduleForm
|
||||
template={template}
|
||||
error={submitScheduleMutation.error}
|
||||
initialValues={{
|
||||
...getAutostart(workspace),
|
||||
...getAutostop(workspace),
|
||||
}}
|
||||
isLoading={submitScheduleMutation.isPending}
|
||||
defaultTTL={dayjs.duration(template.default_ttl_ms, "ms").asHours()}
|
||||
onCancel={() => {
|
||||
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);
|
||||
await submitScheduleMutation.mutateAsync(data);
|
||||
|
||||
if (
|
||||
data.autostopChanged &&
|
||||
getAutostop(workspace).autostopEnabled
|
||||
) {
|
||||
setIsConfirmingApply(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
if (
|
||||
data.autostopChanged &&
|
||||
getAutostop(workspace).autostopEnabled
|
||||
) {
|
||||
setIsConfirmingApply(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
<ConfirmDialog
|
||||
open={isConfirmingApply}
|
||||
|
@ -1411,6 +1411,14 @@ export const MockWorkspace: TypesGen.Workspace = {
|
||||
deleting_at: null,
|
||||
dormant_at: null,
|
||||
next_start_at: null,
|
||||
is_prebuild: false,
|
||||
};
|
||||
|
||||
export const MockPrebuiltWorkspace = {
|
||||
...MockWorkspace,
|
||||
owner_name: "prebuilds",
|
||||
name: "prebuilt-workspace",
|
||||
is_prebuild: true,
|
||||
};
|
||||
|
||||
export const MockFavoriteWorkspace: TypesGen.Workspace = {
|
||||
|
Reference in New Issue
Block a user