From 496473f425fc5a44c7781c0031150c2d80e555ce Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 12 May 2025 16:20:26 +0000 Subject: [PATCH 01/14] Refactor base menu component --- site/src/api/api.ts | 6 +- site/src/api/queries/authCheck.ts | 11 +- site/src/api/queries/workspaces.ts | 16 +++ .../WorkspaceMoreActions.tsx | 101 ++++++++++++++++++ site/src/modules/workspaces/permissions.ts | 50 +++++++++ .../pages/WorkspacePage/Workspace.stories.tsx | 2 +- site/src/pages/WorkspacePage/Workspace.tsx | 5 +- .../WorkspaceDeleteDialog.tsx | 6 +- .../WorkspaceNotifications.tsx | 2 +- .../src/pages/WorkspacePage/WorkspacePage.tsx | 15 ++- .../WorkspacePage/WorkspaceReadyPage.tsx | 11 +- .../pages/WorkspacePage/WorkspaceTopbar.tsx | 6 +- site/src/pages/WorkspacePage/permissions.ts | 39 ------- .../WorkspaceParametersPage.tsx | 2 +- 14 files changed, 196 insertions(+), 76 deletions(-) create mode 100644 site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx create mode 100644 site/src/modules/workspaces/permissions.ts delete mode 100644 site/src/pages/WorkspacePage/permissions.ts diff --git a/site/src/api/api.ts b/site/src/api/api.ts index ef15beb8166f5..b4e5d770326fb 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -482,10 +482,10 @@ class ApiMethods { return response.data; }; - checkAuthorization = async ( + checkAuthorization = async ( params: TypesGen.AuthorizationRequest, - ): Promise => { - const response = await this.axios.post( + ) => { + const response = await this.axios.post( "/api/v2/authcheck", params, ); diff --git a/site/src/api/queries/authCheck.ts b/site/src/api/queries/authCheck.ts index 11f5fafa7d25a..49b08a0e869ca 100644 --- a/site/src/api/queries/authCheck.ts +++ b/site/src/api/queries/authCheck.ts @@ -1,14 +1,19 @@ import { API } from "api/api"; -import type { AuthorizationRequest } from "api/typesGenerated"; +import type { + AuthorizationRequest, + AuthorizationResponse, +} from "api/typesGenerated"; const AUTHORIZATION_KEY = "authorization"; export const getAuthorizationKey = (req: AuthorizationRequest) => [AUTHORIZATION_KEY, req] as const; -export const checkAuthorization = (req: AuthorizationRequest) => { +export const checkAuthorization = ( + req: AuthorizationRequest, +) => { return { queryKey: getAuthorizationKey(req), - queryFn: () => API.checkAuthorization(req), + queryFn: () => API.checkAuthorization(req), }; }; diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index 39008f5c712a3..668fe77092fae 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -19,6 +19,11 @@ import type { } from "react-query"; import { disabledRefetchOptions } from "./util"; import { workspaceBuildsKey } from "./workspaceBuilds"; +import { + workspaceChecks, + type WorkspacePermissions, +} from "modules/workspaces/permissions"; +import { checkAuthorization } from "./authCheck"; export const workspaceByOwnerAndNameKey = (owner: string, name: string) => [ "workspace", @@ -390,3 +395,14 @@ export const workspaceUsage = (options: WorkspaceUsageOptions) => { refetchIntervalInBackground: true, }; }; + +export const workspacePermissions = (workspace?: Workspace) => { + return { + ...checkAuthorization({ + checks: workspace ? workspaceChecks(workspace) : {}, + }), + queryKey: ["workspaces", workspace?.id, "permissions"], + enabled: !!workspace, + staleTime: Number.POSITIVE_INFINITY, + }; +}; diff --git a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx new file mode 100644 index 0000000000000..41d5f3c7c86cb --- /dev/null +++ b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx @@ -0,0 +1,101 @@ +import { Button } from "components/Button/Button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "components/DropdownMenu/DropdownMenu"; +import { + EllipsisVertical, + SettingsIcon, + HistoryIcon, + TrashIcon, + CopyIcon, + DownloadIcon, +} from "lucide-react"; +import { useState, type FC } from "react"; +import { Link as RouterLink } from "react-router-dom"; + +type WorkspaceMoreActionsProps = { + isDuplicationReady: boolean; + disabled?: boolean; + onDuplicate: () => void; + onDelete: () => void; + onChangeVersion?: () => void; + permissions?: { + changeWorkspaceVersion?: boolean; + }; +}; + +export const WorkspaceMoreActions: FC = ({ + disabled, + isDuplicationReady, + onDuplicate, + onDelete, + onChangeVersion, + permissions, +}) => { + // Download logs + const [isDownloadDialogOpen, setIsDownloadDialogOpen] = useState(false); + const canChangeVersion = permissions?.changeWorkspaceVersion !== false; + + return ( + <> + + + + + + + + + + Settings + + + + {onChangeVersion && canChangeVersion && ( + + + Change version… + + )} + + + + Duplicate… + + + setIsDownloadDialogOpen(true)}> + + Download logs… + + + + + + + Delete… + + + + + ); +}; diff --git a/site/src/modules/workspaces/permissions.ts b/site/src/modules/workspaces/permissions.ts new file mode 100644 index 0000000000000..07c4e612cdf61 --- /dev/null +++ b/site/src/modules/workspaces/permissions.ts @@ -0,0 +1,50 @@ +import type { AuthorizationCheck, Workspace } from "api/typesGenerated"; + +export const workspaceChecks = (workspace: Workspace) => + ({ + readWorkspace: { + object: { + resource_type: "workspace", + resource_id: workspace.id, + owner_id: workspace.owner_id, + }, + action: "read", + }, + updateWorkspace: { + object: { + resource_type: "workspace", + resource_id: workspace.id, + owner_id: workspace.owner_id, + }, + action: "update", + }, + updateWorkspaceVersion: { + object: { + resource_type: "template", + resource_id: workspace.template_id, + }, + action: "update", + }, + // We only want to allow template admins to delete failed workspaces since + // they can leave orphaned resources. + deleteFailedWorkspace: { + object: { + resource_type: "template", + resource_id: workspace.template_id, + }, + action: "update", + }, + // To run a build in debug mode we need to be able to read the deployment + // config (enable_terraform_debug_mode). + deploymentConfig: { + object: { + resource_type: "deployment_config", + }, + action: "read", + }, + }) satisfies Record; + +export type WorkspacePermissions = Record< + keyof ReturnType, + boolean +>; diff --git a/site/src/pages/WorkspacePage/Workspace.stories.tsx b/site/src/pages/WorkspacePage/Workspace.stories.tsx index 957a651788d2c..395f1bed99acb 100644 --- a/site/src/pages/WorkspacePage/Workspace.stories.tsx +++ b/site/src/pages/WorkspacePage/Workspace.stories.tsx @@ -5,7 +5,7 @@ import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext"; import * as Mocks from "testHelpers/entities"; import { withAuthProvider, withDashboardProvider } from "testHelpers/storybook"; import { Workspace } from "./Workspace"; -import type { WorkspacePermissions } from "./permissions"; +import type { WorkspacePermissions } from "../../modules/workspaces/permissions"; // Helper function to create timestamps easily - Copied from AppStatuses.stories.tsx const createTimestamp = ( diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index 69ce29ed0e7d1..c4d7b4b7ab0ae 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -24,7 +24,7 @@ import { } from "./WorkspaceBuildProgress"; import { WorkspaceDeletedBanner } from "./WorkspaceDeletedBanner"; import { WorkspaceTopbar } from "./WorkspaceTopbar"; -import type { WorkspacePermissions } from "./permissions"; +import type { WorkspacePermissions } from "../../modules/workspaces/permissions"; import { resourceOptionValue, useResourcesNav } from "./useResourcesNav"; export interface WorkspaceProps { @@ -41,7 +41,6 @@ export interface WorkspaceProps { isUpdating: boolean; isRestarting: boolean; workspace: TypesGen.Workspace; - canChangeVersions: boolean; hideSSHButton?: boolean; hideVSCodeDesktopButton?: boolean; buildInfo?: TypesGen.BuildInfoResponse; @@ -73,7 +72,6 @@ export const Workspace: FC = ({ workspace, isUpdating, isRestarting, - canChangeVersions, hideSSHButton, hideVSCodeDesktopButton, buildInfo, @@ -155,7 +153,6 @@ export const Workspace: FC = ({ handleDormantActivate={handleDormantActivate} handleToggleFavorite={handleToggleFavorite} canDebugMode={canDebugMode} - canChangeVersions={canChangeVersions} isUpdating={isUpdating} isRestarting={isRestarting} canUpdateWorkspace={permissions.updateWorkspace} diff --git a/site/src/pages/WorkspacePage/WorkspaceDeleteDialog/WorkspaceDeleteDialog.tsx b/site/src/pages/WorkspacePage/WorkspaceDeleteDialog/WorkspaceDeleteDialog.tsx index 2b2e70c721bf7..902d1c8e25d7e 100644 --- a/site/src/pages/WorkspacePage/WorkspaceDeleteDialog/WorkspaceDeleteDialog.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceDeleteDialog/WorkspaceDeleteDialog.tsx @@ -12,7 +12,7 @@ import { docs } from "utils/docs"; interface WorkspaceDeleteDialogProps { workspace: Workspace; - canUpdateTemplate: boolean; + canDeleteFailedWorkspace: boolean; isOpen: boolean; onCancel: () => void; onConfirm: (arg: CreateWorkspaceBuildRequest["orphan"]) => void; @@ -21,7 +21,7 @@ interface WorkspaceDeleteDialogProps { export const WorkspaceDeleteDialog: FC = ({ workspace, - canUpdateTemplate, + canDeleteFailedWorkspace, isOpen, onCancel, onConfirm, @@ -102,7 +102,7 @@ export const WorkspaceDeleteDialog: FC = ({ // Orphaning is sort of a "last resort" that should really only // be used if Terraform is failing to apply while deleting, which // usually means that builds are failing as well. - canUpdateTemplate && + canDeleteFailedWorkspace && workspace.latest_build.status === "failed" && (
diff --git a/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.tsx b/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.tsx index 7c72c9777aaeb..976e7bac4fcbb 100644 --- a/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.tsx @@ -15,7 +15,7 @@ import { useDashboard } from "modules/dashboard/useDashboard"; import { TemplateUpdateMessage } from "modules/templates/TemplateUpdateMessage"; import { type FC, useEffect, useState } from "react"; import { useQuery } from "react-query"; -import type { WorkspacePermissions } from "../permissions"; +import type { WorkspacePermissions } from "../../../modules/workspaces/permissions"; import { NotificationActionButton, type NotificationItem, diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index e0b5cf1d14bfe..aa32c06e264f7 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -2,7 +2,10 @@ import { watchWorkspace } from "api/api"; import { checkAuthorization } from "api/queries/authCheck"; import { template as templateQueryOptions } from "api/queries/templates"; import { workspaceBuildsKey } from "api/queries/workspaceBuilds"; -import { workspaceByOwnerAndName } from "api/queries/workspaces"; +import { + workspaceByOwnerAndName, + workspacePermissions, +} from "api/queries/workspaces"; import type { Workspace } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { displayError } from "components/GlobalSnackbar/utils"; @@ -15,7 +18,6 @@ import { type FC, useEffect } from "react"; import { useQuery, useQueryClient } from "react-query"; import { useParams } from "react-router-dom"; import { WorkspaceReadyPage } from "./WorkspaceReadyPage"; -import { type WorkspacePermissions, workspaceChecks } from "./permissions"; const WorkspacePage: FC = () => { const queryClient = useQueryClient(); @@ -43,13 +45,8 @@ const WorkspacePage: FC = () => { const template = templateQuery.data; // Permissions - const checks = - workspace && template ? workspaceChecks(workspace, template) : {}; - const permissionsQuery = useQuery({ - ...checkAuthorization({ checks }), - enabled: workspace !== undefined && template !== undefined, - }); - const permissions = permissionsQuery.data as WorkspacePermissions | undefined; + const permissionsQuery = useQuery(workspacePermissions(workspace)); + const permissions = permissionsQuery.data; // Watch workspace changes const updateWorkspaceData = useEffectEvent( diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 9ae072e420dd1..75852516d1b8e 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -36,7 +36,7 @@ import { ChangeVersionDialog } from "./ChangeVersionDialog"; import { UpdateBuildParametersDialog } from "./UpdateBuildParametersDialog"; import { Workspace } from "./Workspace"; import { WorkspaceDeleteDialog } from "./WorkspaceDeleteDialog"; -import type { WorkspacePermissions } from "./permissions"; +import type { WorkspacePermissions } from "modules/workspaces/permissions"; interface WorkspaceReadyPageProps { template: TypesGen.Template; @@ -62,7 +62,7 @@ export const WorkspaceReadyPage: FC = ({ // Debug mode const { data: deploymentValues } = useQuery({ ...deploymentConfig(), - enabled: permissions.viewDeploymentConfig, + enabled: permissions.deploymentConfig, }); // Build logs @@ -99,7 +99,6 @@ export const WorkspaceReadyPage: FC = ({ }, []); // Change version - const canChangeVersions = permissions.updateTemplate; const [changeVersionDialogOpen, setChangeVersionDialogOpen] = useState(false); const changeVersionMutation = useMutation( changeVersion(workspace, queryClient), @@ -121,9 +120,6 @@ export const WorkspaceReadyPage: FC = ({ latestVersion, }); - // If a user can update the template then they can force a delete - // (via orphan). - const canUpdateTemplate = Boolean(permissions.updateTemplate); const [isConfirmingDelete, setIsConfirmingDelete] = useState(false); const deleteWorkspaceMutation = useMutation( deleteWorkspace(workspace, queryClient), @@ -267,7 +263,6 @@ export const WorkspaceReadyPage: FC = ({ toggleFavoriteMutation.mutate(); }} latestVersion={latestVersion} - canChangeVersions={canChangeVersions} hideSSHButton={featureVisibility.browser_only} hideVSCodeDesktopButton={featureVisibility.browser_only} buildInfo={buildInfoQuery.data} @@ -279,7 +274,7 @@ export const WorkspaceReadyPage: FC = ({ { setIsConfirmingDelete(false); diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index 2af8d694e120e..b3966639a946d 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -28,7 +28,7 @@ import { displayDormantDeletion } from "utils/dormant"; import { WorkspaceActions } from "./WorkspaceActions/WorkspaceActions"; import { WorkspaceNotifications } from "./WorkspaceNotifications/WorkspaceNotifications"; import { WorkspaceScheduleControls } from "./WorkspaceScheduleControls"; -import type { WorkspacePermissions } from "./permissions"; +import type { WorkspacePermissions } from "../../modules/workspaces/permissions"; export type WorkspaceError = | "getBuildsError" @@ -51,7 +51,6 @@ export interface WorkspaceProps { isRestarting: boolean; workspace: TypesGen.Workspace; canUpdateWorkspace: boolean; - canChangeVersions: boolean; canDebugMode: boolean; handleRetry: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; handleDebug: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; @@ -76,7 +75,6 @@ export const WorkspaceTopbar: FC = ({ isUpdating, isRestarting, canUpdateWorkspace, - canChangeVersions, canDebugMode, handleRetry, handleDebug, @@ -257,7 +255,7 @@ export const WorkspaceTopbar: FC = ({ handleDormantActivate={handleDormantActivate} handleToggleFavorite={handleToggleFavorite} canDebug={canDebugMode} - canChangeVersions={canChangeVersions} + canChangeVersions={permissions.updateWorkspaceVersion} isUpdating={isUpdating} isRestarting={isRestarting} /> diff --git a/site/src/pages/WorkspacePage/permissions.ts b/site/src/pages/WorkspacePage/permissions.ts deleted file mode 100644 index 3ac1df5a3a7fd..0000000000000 --- a/site/src/pages/WorkspacePage/permissions.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { Template, Workspace } from "api/typesGenerated"; - -export const workspaceChecks = (workspace: Workspace, template: Template) => - ({ - readWorkspace: { - object: { - resource_type: "workspace", - resource_id: workspace.id, - owner_id: workspace.owner_id, - }, - action: "read", - }, - updateWorkspace: { - object: { - resource_type: "workspace", - resource_id: workspace.id, - owner_id: workspace.owner_id, - }, - action: "update", - }, - updateTemplate: { - object: { - resource_type: "template", - resource_id: template.id, - }, - action: "update", - }, - viewDeploymentConfig: { - object: { - resource_type: "deployment_config", - }, - action: "read", - }, - }) as const; - -export type WorkspacePermissions = Record< - keyof ReturnType, - boolean ->; diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx index a3bc7964f9558..8377d7266e02e 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx @@ -18,7 +18,7 @@ import { pageTitle } from "utils/page"; import { type WorkspacePermissions, workspaceChecks, -} from "../../WorkspacePage/permissions"; +} from "../../../modules/workspaces/permissions"; import { useWorkspaceSettings } from "../WorkspaceSettingsLayout"; import { WorkspaceParametersForm, From 22759e37790e40b0d15b64d868eaf7484399d3a2 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 12 May 2025 18:00:45 +0000 Subject: [PATCH 02/14] Refactor change workspace version --- site/src/api/queries/templates.ts | 7 +- .../ChangeWorkspaceVersionDialog.stories.tsx | 68 +++++++++++++++++ .../ChangeWorkspaceVersionDialog.tsx} | 72 +++++++++--------- .../WorkspaceMoreActions.tsx | 76 +++++++++++++++++-- .../ChangeVersionDialog.stories.tsx | 49 ------------ site/src/pages/WorkspacePage/Workspace.tsx | 3 - .../WorkspaceActions/WorkspaceActions.tsx | 10 --- .../WorkspacePage/WorkspaceReadyPage.tsx | 52 +------------ .../pages/WorkspacePage/WorkspaceTopbar.tsx | 3 - 9 files changed, 177 insertions(+), 163 deletions(-) create mode 100644 site/src/modules/workspaces/WorkspaceMoreActions/ChangeWorkspaceVersionDialog.stories.tsx rename site/src/{pages/WorkspacePage/ChangeVersionDialog.tsx => modules/workspaces/WorkspaceMoreActions/ChangeWorkspaceVersionDialog.tsx} (71%) delete mode 100644 site/src/pages/WorkspacePage/ChangeVersionDialog.stories.tsx diff --git a/site/src/api/queries/templates.ts b/site/src/api/queries/templates.ts index 72e5deaefc72a..a99eead5f1816 100644 --- a/site/src/api/queries/templates.ts +++ b/site/src/api/queries/templates.ts @@ -139,9 +139,14 @@ export const templateVersionByName = ( }; }; +export const templateVersionsQueryKey = (templateId: string) => [ + "templateVersions", + templateId, +]; + export const templateVersions = (templateId: string) => { return { - queryKey: ["templateVersions", templateId], + queryKey: templateVersionsQueryKey(templateId), queryFn: () => API.getTemplateVersions(templateId), }; }; diff --git a/site/src/modules/workspaces/WorkspaceMoreActions/ChangeWorkspaceVersionDialog.stories.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/ChangeWorkspaceVersionDialog.stories.tsx new file mode 100644 index 0000000000000..6c85fa7d571f0 --- /dev/null +++ b/site/src/modules/workspaces/WorkspaceMoreActions/ChangeWorkspaceVersionDialog.stories.tsx @@ -0,0 +1,68 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { + MockTemplate, + MockTemplateVersion, + MockTemplateVersionWithMarkdownMessage, + MockWorkspace, +} from "testHelpers/entities"; +import { ChangeWorkspaceVersionDialog } from "./ChangeWorkspaceVersionDialog"; +import { templateVersionsQueryKey } from "api/queries/templates"; + +const noMessage = { + ...MockTemplateVersion, + id: "no-message", + message: "", +}; + +const meta: Meta = { + title: "modules/workspaces/ChangeWorkspaceVersionDialog", + component: ChangeWorkspaceVersionDialog, + args: { + open: true, + workspace: MockWorkspace, + }, + parameters: { + queries: [ + { + key: templateVersionsQueryKey(MockWorkspace.template_id), + data: [ + MockTemplateVersion, + MockTemplateVersionWithMarkdownMessage, + noMessage, + ], + }, + ], + }, +}; + +export default meta; +type Story = StoryObj; + +export const NoVersionSelected: Story = {}; + +export const NoMessage: Story = { + args: { + workspace: { + ...MockWorkspace, + template_active_version_id: noMessage.id, + }, + }, +}; + +export const ShortMessage: Story = { + args: { + workspace: { + ...MockWorkspace, + template_active_version_id: MockTemplateVersion.id, + }, + }, +}; + +export const LongMessage: Story = { + args: { + workspace: { + ...MockWorkspace, + template_active_version_id: MockTemplateVersionWithMarkdownMessage.id, + }, + }, +}; diff --git a/site/src/pages/WorkspacePage/ChangeVersionDialog.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/ChangeWorkspaceVersionDialog.tsx similarity index 71% rename from site/src/pages/WorkspacePage/ChangeVersionDialog.tsx rename to site/src/modules/workspaces/WorkspaceMoreActions/ChangeWorkspaceVersionDialog.tsx index 7990295620d65..2a55b87a212fa 100644 --- a/site/src/pages/WorkspacePage/ChangeVersionDialog.tsx +++ b/site/src/modules/workspaces/WorkspaceMoreActions/ChangeWorkspaceVersionDialog.tsx @@ -3,7 +3,8 @@ import AlertTitle from "@mui/material/AlertTitle"; import Autocomplete from "@mui/material/Autocomplete"; import CircularProgress from "@mui/material/CircularProgress"; import TextField from "@mui/material/TextField"; -import type { Template, TemplateVersion } from "api/typesGenerated"; +import { templateVersions } from "api/queries/templates"; +import type { TemplateVersion, Workspace } from "api/typesGenerated"; import { Alert } from "components/Alert/Alert"; import { Avatar } from "components/Avatar/Avatar"; import { AvatarData } from "components/Avatar/AvatarData"; @@ -15,41 +16,40 @@ import { Pill } from "components/Pill/Pill"; import { Stack } from "components/Stack/Stack"; import { InfoIcon } from "lucide-react"; import { TemplateUpdateMessage } from "modules/templates/TemplateUpdateMessage"; -import { type FC, useRef, useState } from "react"; +import { type FC, useEffect, useState } from "react"; +import { useQuery } from "react-query"; import { createDayString } from "utils/createDayString"; -export type ChangeVersionDialogProps = DialogProps & { - template: Template | undefined; - templateVersions: TemplateVersion[] | undefined; - defaultTemplateVersion: TemplateVersion | undefined; +export type ChangeWorkspaceVersionDialogProps = DialogProps & { + workspace: Workspace; onClose: () => void; - onConfirm: (templateVersion: TemplateVersion) => void; + onConfirm: (version: TemplateVersion) => void; }; -export const ChangeVersionDialog: FC = ({ - onConfirm, - onClose, - template, - templateVersions, - defaultTemplateVersion, - ...dialogProps -}) => { +export const ChangeWorkspaceVersionDialog: FC< + ChangeWorkspaceVersionDialogProps +> = ({ workspace, onClose, onConfirm, ...dialogProps }) => { + const { data: versions } = useQuery(templateVersions(workspace.template_id)); const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false); - const selectedTemplateVersion = useRef( - defaultTemplateVersion, + const activeVersion = versions?.find( + (v) => workspace.template_active_version_id === v.id, ); - const version = selectedTemplateVersion.current; - const validTemplateVersions = templateVersions?.filter((version) => { - return version.job.status === "succeeded"; - }); + const [selectedVersion, setSelectedVersion] = useState(); + const validVersions = versions?.filter((v) => v.job.status === "succeeded"); + + useEffect(() => { + if (activeVersion) { + setSelectedVersion(activeVersion); + } + }, [activeVersion]); return ( { - if (selectedTemplateVersion.current) { - onConfirm(selectedTemplateVersion.current); + if (selectedVersion) { + onConfirm(selectedVersion); } }} hideCancel={false} @@ -60,18 +60,17 @@ export const ChangeVersionDialog: FC = ({ description={

You are about to change the version of this workspace.

- {validTemplateVersions ? ( + {validVersions ? ( <> { - selectedTemplateVersion.current = - newTemplateVersion ?? undefined; + setSelectedVersion(newTemplateVersion); }} onOpen={() => { setIsAutocompleteOpen(true); @@ -112,9 +111,8 @@ export const ChangeVersionDialog: FC = ({ /> )}
- {template?.active_version_id === option.id && ( - Active - )} + {workspace.template_active_version_id === + option.id && Active} } subtitle={createDayString(option.created_at)} @@ -131,9 +129,7 @@ export const ChangeVersionDialog: FC = ({ ...params.InputProps, endAdornment: ( <> - {!templateVersions ? ( - - ) : null} + {!versions && } {params.InputProps.endAdornment} ), @@ -144,16 +140,16 @@ export const ChangeVersionDialog: FC = ({ )} /> - {version && ( + {selectedVersion && ( <> - {version.message && ( + {selectedVersion.message && ( - {version.message} + {selectedVersion.message} )} - Published by {version.created_by.username} + Published by {selectedVersion.created_by.username} diff --git a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx index 41d5f3c7c86cb..82e7a1c749548 100644 --- a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx +++ b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx @@ -1,3 +1,6 @@ +import { MissingBuildParameters } from "api/api"; +import { changeVersion } from "api/queries/workspaces"; +import type { Workspace } from "api/typesGenerated"; import { Button } from "components/Button/Button"; import { DropdownMenu, @@ -14,31 +17,42 @@ import { CopyIcon, DownloadIcon, } from "lucide-react"; +import { UpdateBuildParametersDialog } from "pages/WorkspacePage/UpdateBuildParametersDialog"; +import { DownloadLogsDialog } from "pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog"; import { useState, type FC } from "react"; +import { useMutation, useQueryClient } from "react-query"; import { Link as RouterLink } from "react-router-dom"; +import { ChangeWorkspaceVersionDialog } from "./ChangeWorkspaceVersionDialog"; type WorkspaceMoreActionsProps = { + workspace: Workspace; isDuplicationReady: boolean; disabled?: boolean; - onDuplicate: () => void; - onDelete: () => void; - onChangeVersion?: () => void; permissions?: { changeWorkspaceVersion?: boolean; }; + onDuplicate: () => void; + onDelete: () => void; }; export const WorkspaceMoreActions: FC = ({ + workspace, disabled, + permissions, isDuplicationReady, onDuplicate, onDelete, - onChangeVersion, - permissions, }) => { + const queryClient = useQueryClient(); + // Download logs const [isDownloadDialogOpen, setIsDownloadDialogOpen] = useState(false); - const canChangeVersion = permissions?.changeWorkspaceVersion !== false; + + // Change version + const [changeVersionDialogOpen, setChangeVersionDialogOpen] = useState(false); + const changeVersionMutation = useMutation( + changeVersion(workspace, queryClient), + ); return ( <> @@ -64,8 +78,12 @@ export const WorkspaceMoreActions: FC = ({ - {onChangeVersion && canChangeVersion && ( - + {permissions?.changeWorkspaceVersion && ( + { + setChangeVersionDialogOpen(true); + }} + > Change version… @@ -96,6 +114,48 @@ export const WorkspaceMoreActions: FC = ({ + + setIsDownloadDialogOpen(false)} + /> + + { + changeVersionMutation.reset(); + }} + onUpdate={(buildParameters) => { + if (isMissingBuildParameters(changeVersionMutation.error)) { + changeVersionMutation.mutate({ + versionId: changeVersionMutation.error.versionId, + buildParameters, + }); + } + }} + /> + + { + setChangeVersionDialogOpen(false); + }} + onConfirm={(version) => { + setChangeVersionDialogOpen(false); + changeVersionMutation.mutate({ versionId: version.id }); + }} + /> ); }; + +const isMissingBuildParameters = (e: unknown): e is MissingBuildParameters => { + return Boolean(e && e instanceof MissingBuildParameters); +}; diff --git a/site/src/pages/WorkspacePage/ChangeVersionDialog.stories.tsx b/site/src/pages/WorkspacePage/ChangeVersionDialog.stories.tsx deleted file mode 100644 index 2da7ae322c22e..0000000000000 --- a/site/src/pages/WorkspacePage/ChangeVersionDialog.stories.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { - MockTemplate, - MockTemplateVersion, - MockTemplateVersionWithMarkdownMessage, -} from "testHelpers/entities"; -import { ChangeVersionDialog } from "./ChangeVersionDialog"; - -const noMessage = { - ...MockTemplateVersion, - message: "", -}; - -const meta: Meta = { - title: "pages/WorkspacePage/ChangeVersionDialog", - component: ChangeVersionDialog, - args: { - open: true, - template: MockTemplate, - templateVersions: [ - MockTemplateVersion, - MockTemplateVersionWithMarkdownMessage, - noMessage, - ], - }, -}; - -export default meta; -type Story = StoryObj; - -export const NoVersionSelected: Story = {}; - -export const NoMessage: Story = { - args: { - defaultTemplateVersion: noMessage, - }, -}; - -export const ShortMessage: Story = { - args: { - defaultTemplateVersion: MockTemplateVersion, - }, -}; - -export const LongMessage: Story = { - args: { - defaultTemplateVersion: MockTemplateVersionWithMarkdownMessage, - }, -}; diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index c4d7b4b7ab0ae..a180450ce5482 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -35,7 +35,6 @@ export interface WorkspaceProps { handleUpdate: () => void; handleCancel: () => void; handleSettings: () => void; - handleChangeVersion: () => void; handleDormantActivate: () => void; handleToggleFavorite: () => void; isUpdating: boolean; @@ -66,7 +65,6 @@ export const Workspace: FC = ({ handleUpdate, handleCancel, handleSettings, - handleChangeVersion, handleDormantActivate, handleToggleFavorite, workspace, @@ -149,7 +147,6 @@ export const Workspace: FC = ({ handleSettings={handleSettings} handleRetry={handleRetry} handleDebug={handleDebug} - handleChangeVersion={handleChangeVersion} handleDormantActivate={handleDormantActivate} handleToggleFavorite={handleToggleFavorite} canDebugMode={canDebugMode} diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx index feb77c65124cb..5876e4411ab05 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx @@ -47,7 +47,6 @@ export interface WorkspaceActionsProps { handleUpdate: () => void; handleCancel: () => void; handleSettings: () => void; - handleChangeVersion: () => void; handleRetry: (buildParameters?: WorkspaceBuildParameter[]) => void; handleDebug: (buildParameters?: WorkspaceBuildParameter[]) => void; handleDormantActivate: () => void; @@ -70,7 +69,6 @@ export const WorkspaceActions: FC = ({ handleSettings, handleRetry, handleDebug, - handleChangeVersion, handleDormantActivate, isUpdating, isRestarting, @@ -200,13 +198,6 @@ export const WorkspaceActions: FC = ({ Settings - {canChangeVersions && ( - - - Change version… - - )} - = ({ workspace={workspace} open={isDownloadDialogOpen} onClose={() => setIsDownloadDialogOpen(false)} - onConfirm={() => {}} />
); diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 75852516d1b8e..592ec64ec5b5b 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -32,8 +32,6 @@ import { Helmet } from "react-helmet-async"; import { useMutation, useQuery, useQueryClient } from "react-query"; import { useNavigate } from "react-router-dom"; import { pageTitle } from "utils/page"; -import { ChangeVersionDialog } from "./ChangeVersionDialog"; -import { UpdateBuildParametersDialog } from "./UpdateBuildParametersDialog"; import { Workspace } from "./Workspace"; import { WorkspaceDeleteDialog } from "./WorkspaceDeleteDialog"; import type { WorkspacePermissions } from "modules/workspaces/permissions"; @@ -98,17 +96,8 @@ export const WorkspaceReadyPage: FC = ({ setFaviconTheme(isDark.matches ? "light" : "dark"); }, []); - // Change version - const [changeVersionDialogOpen, setChangeVersionDialogOpen] = useState(false); - const changeVersionMutation = useMutation( - changeVersion(workspace, queryClient), - ); - // Versions - const { data: allVersions } = useQuery({ - ...templateVersions(workspace.template_id), - enabled: changeVersionDialogOpen, - }); + const { data: latestVersion } = useQuery({ ...templateVersion(workspace.template_active_version_id), enabled: workspace.outdated, @@ -248,9 +237,6 @@ export const WorkspaceReadyPage: FC = ({ canDebugMode={ deploymentValues?.config.enable_terraform_debug_mode ?? false } - handleChangeVersion={() => { - setChangeVersionDialogOpen(true); - }} handleDormantActivate={async () => { try { await activateWorkspaceMutation.mutateAsync(); @@ -286,42 +272,6 @@ export const WorkspaceReadyPage: FC = ({ workspaceBuildDateStr={dayjs(workspace.created_at).fromNow()} /> - { - changeVersionMutation.reset(); - }} - onUpdate={(buildParameters) => { - if (changeVersionMutation.error instanceof MissingBuildParameters) { - changeVersionMutation.mutate({ - versionId: changeVersionMutation.error.versionId, - buildParameters, - }); - } - }} - /> - - workspace.latest_build.template_version_id === v.id, - )} - open={changeVersionDialogOpen} - onClose={() => { - setChangeVersionDialogOpen(false); - }} - onConfirm={(templateVersion) => { - setChangeVersionDialogOpen(false); - changeVersionMutation.mutate({ versionId: templateVersion.id }); - }} - /> - { diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index b3966639a946d..f7fc892b278c4 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -45,7 +45,6 @@ export interface WorkspaceProps { handleUpdate: () => void; handleCancel: () => void; handleSettings: () => void; - handleChangeVersion: () => void; handleDormantActivate: () => void; isUpdating: boolean; isRestarting: boolean; @@ -68,7 +67,6 @@ export const WorkspaceTopbar: FC = ({ handleUpdate, handleCancel, handleSettings, - handleChangeVersion, handleDormantActivate, handleToggleFavorite, workspace, @@ -251,7 +249,6 @@ export const WorkspaceTopbar: FC = ({ handleSettings={handleSettings} handleRetry={handleRetry} handleDebug={handleDebug} - handleChangeVersion={handleChangeVersion} handleDormantActivate={handleDormantActivate} handleToggleFavorite={handleToggleFavorite} canDebug={canDebugMode} From 2f3cce0dcc1ac344b4df27949b22cc0f52963da0 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 12 May 2025 18:17:23 +0000 Subject: [PATCH 03/14] Refactor delete workspace --- .../WorkspaceDeleteDialog.stories.tsx | 20 ++++++----- .../WorkspaceDeleteDialog.tsx | 5 ++- .../WorkspaceMoreActions.tsx | 35 ++++++++++++++----- site/src/pages/WorkspacePage/Workspace.tsx | 3 -- .../WorkspaceActions/WorkspaceActions.tsx | 29 --------------- .../WorkspaceDeleteDialog/index.ts | 1 - .../WorkspacePage/WorkspaceReadyPage.tsx | 18 +--------- .../pages/WorkspacePage/WorkspaceTopbar.tsx | 3 -- 8 files changed, 42 insertions(+), 72 deletions(-) rename site/src/{pages/WorkspacePage/WorkspaceDeleteDialog => modules/workspaces/WorkspaceMoreActions}/WorkspaceDeleteDialog.stories.tsx (70%) rename site/src/{pages/WorkspacePage/WorkspaceDeleteDialog => modules/workspaces/WorkspaceMoreActions}/WorkspaceDeleteDialog.tsx (98%) delete mode 100644 site/src/pages/WorkspacePage/WorkspaceDeleteDialog/index.ts diff --git a/site/src/pages/WorkspacePage/WorkspaceDeleteDialog/WorkspaceDeleteDialog.stories.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceDeleteDialog.stories.tsx similarity index 70% rename from site/src/pages/WorkspacePage/WorkspaceDeleteDialog/WorkspaceDeleteDialog.stories.tsx rename to site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceDeleteDialog.stories.tsx index 7cce5ec48cf04..e6aaa28ee4247 100644 --- a/site/src/pages/WorkspacePage/WorkspaceDeleteDialog/WorkspaceDeleteDialog.stories.tsx +++ b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceDeleteDialog.stories.tsx @@ -1,17 +1,21 @@ import type { Meta, StoryObj } from "@storybook/react"; import { MockFailedWorkspace, MockWorkspace } from "testHelpers/entities"; import { WorkspaceDeleteDialog } from "./WorkspaceDeleteDialog"; +import { daysAgo } from "utils/time"; const meta: Meta = { - title: "pages/WorkspacePage/WorkspaceDeleteDialog", + title: "modules/workspaces/WorkspaceDeleteDialog", component: WorkspaceDeleteDialog, args: { - workspace: MockWorkspace, - canUpdateTemplate: false, + workspace: { + ...MockWorkspace, + latest_build: { + ...MockWorkspace.latest_build, + created_at: daysAgo(2), + }, + }, + canDeleteFailedWorkspace: false, isOpen: true, - onCancel: () => {}, - onConfirm: () => {}, - workspaceBuildDateStr: "2 days ago", }, }; @@ -30,7 +34,7 @@ export const Unhealthy: Story = { // Should look the same as `Example` export const AdminView: Story = { args: { - canUpdateTemplate: true, + canDeleteFailedWorkspace: true, }, }; @@ -38,6 +42,6 @@ export const AdminView: Story = { export const UnhealthyAdminView: Story = { args: { workspace: MockFailedWorkspace, - canUpdateTemplate: true, + canDeleteFailedWorkspace: true, }, }; diff --git a/site/src/pages/WorkspacePage/WorkspaceDeleteDialog/WorkspaceDeleteDialog.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceDeleteDialog.tsx similarity index 98% rename from site/src/pages/WorkspacePage/WorkspaceDeleteDialog/WorkspaceDeleteDialog.tsx rename to site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceDeleteDialog.tsx index 902d1c8e25d7e..8f5179b0b64da 100644 --- a/site/src/pages/WorkspacePage/WorkspaceDeleteDialog/WorkspaceDeleteDialog.tsx +++ b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceDeleteDialog.tsx @@ -7,6 +7,7 @@ import type { Workspace, } from "api/typesGenerated"; import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; +import dayjs from "dayjs"; import { type FC, type FormEvent, useId, useState } from "react"; import { docs } from "utils/docs"; @@ -16,7 +17,6 @@ interface WorkspaceDeleteDialogProps { isOpen: boolean; onCancel: () => void; onConfirm: (arg: CreateWorkspaceBuildRequest["orphan"]) => void; - workspaceBuildDateStr: string; } export const WorkspaceDeleteDialog: FC = ({ @@ -25,7 +25,6 @@ export const WorkspaceDeleteDialog: FC = ({ isOpen, onCancel, onConfirm, - workspaceBuildDateStr, }) => { const hookId = useId(); const [userConfirmationText, setUserConfirmationText] = useState(""); @@ -62,7 +61,7 @@ export const WorkspaceDeleteDialog: FC = ({

workspace

-

{workspaceBuildDateStr}

+

{dayjs(workspace.created_at).fromNow()}

created

diff --git a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx index 82e7a1c749548..500b17d6676a9 100644 --- a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx +++ b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx @@ -1,5 +1,5 @@ import { MissingBuildParameters } from "api/api"; -import { changeVersion } from "api/queries/workspaces"; +import { changeVersion, deleteWorkspace } from "api/queries/workspaces"; import type { Workspace } from "api/typesGenerated"; import { Button } from "components/Button/Button"; import { @@ -23,16 +23,15 @@ import { useState, type FC } from "react"; import { useMutation, useQueryClient } from "react-query"; import { Link as RouterLink } from "react-router-dom"; import { ChangeWorkspaceVersionDialog } from "./ChangeWorkspaceVersionDialog"; +import { WorkspaceDeleteDialog } from "./WorkspaceDeleteDialog"; +import type { WorkspacePermissions } from "../permissions"; type WorkspaceMoreActionsProps = { workspace: Workspace; isDuplicationReady: boolean; + permissions: WorkspacePermissions; disabled?: boolean; - permissions?: { - changeWorkspaceVersion?: boolean; - }; onDuplicate: () => void; - onDelete: () => void; }; export const WorkspaceMoreActions: FC = ({ @@ -41,7 +40,6 @@ export const WorkspaceMoreActions: FC = ({ permissions, isDuplicationReady, onDuplicate, - onDelete, }) => { const queryClient = useQueryClient(); @@ -54,6 +52,12 @@ export const WorkspaceMoreActions: FC = ({ changeVersion(workspace, queryClient), ); + // Delete + const [isConfirmingDelete, setIsConfirmingDelete] = useState(false); + const deleteWorkspaceMutation = useMutation( + deleteWorkspace(workspace, queryClient), + ); + return ( <> @@ -78,7 +82,7 @@ export const WorkspaceMoreActions: FC = ({ - {permissions?.changeWorkspaceVersion && ( + {permissions.updateWorkspaceVersion && ( { setChangeVersionDialogOpen(true); @@ -106,7 +110,9 @@ export const WorkspaceMoreActions: FC = ({ { + setIsConfirmingDelete(true); + }} data-testid="delete-button" > @@ -152,6 +158,19 @@ export const WorkspaceMoreActions: FC = ({ changeVersionMutation.mutate({ versionId: version.id }); }} /> + + { + setIsConfirmingDelete(false); + }} + onConfirm={(orphan) => { + deleteWorkspaceMutation.mutate({ orphan }); + setIsConfirmingDelete(false); + }} + /> ); }; diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index a180450ce5482..bb7ca4ac98fa8 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -31,7 +31,6 @@ export interface WorkspaceProps { handleStart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; handleStop: () => void; handleRestart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; - handleDelete: () => void; handleUpdate: () => void; handleCancel: () => void; handleSettings: () => void; @@ -61,7 +60,6 @@ export const Workspace: FC = ({ handleStart, handleStop, handleRestart, - handleDelete, handleUpdate, handleCancel, handleSettings, @@ -141,7 +139,6 @@ export const Workspace: FC = ({ handleStart={handleStart} handleStop={handleStop} handleRestart={handleRestart} - handleDelete={handleDelete} handleUpdate={handleUpdate} handleCancel={handleCancel} handleSettings={handleSettings} diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx index 5876e4411ab05..9624a7c43d98a 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx @@ -1,6 +1,5 @@ import DownloadOutlined from "@mui/icons-material/DownloadOutlined"; import DuplicateIcon from "@mui/icons-material/FileCopyOutlined"; -import HistoryIcon from "@mui/icons-material/HistoryOutlined"; import SettingsIcon from "@mui/icons-material/SettingsOutlined"; import type { Workspace, WorkspaceBuildParameter } from "api/typesGenerated"; import { Button } from "components/Button/Button"; @@ -12,7 +11,6 @@ import { DropdownMenuTrigger, } from "components/DropdownMenu/DropdownMenu"; import { useAuthenticated } from "hooks/useAuthenticated"; -import { TrashIcon } from "lucide-react"; import { EllipsisVertical } from "lucide-react"; import { type ActionType, @@ -34,7 +32,6 @@ import { UpdateButton, } from "./Buttons"; import { DebugButton } from "./DebugButton"; -import { DownloadLogsDialog } from "./DownloadLogsDialog"; import { RetryButton } from "./RetryButton"; export interface WorkspaceActionsProps { @@ -43,7 +40,6 @@ export interface WorkspaceActionsProps { handleStart: (buildParameters?: WorkspaceBuildParameter[]) => void; handleStop: () => void; handleRestart: (buildParameters?: WorkspaceBuildParameter[]) => void; - handleDelete: () => void; handleUpdate: () => void; handleCancel: () => void; handleSettings: () => void; @@ -63,7 +59,6 @@ export const WorkspaceActions: FC = ({ handleStart, handleStop, handleRestart, - handleDelete, handleUpdate, handleCancel, handleSettings, @@ -78,8 +73,6 @@ export const WorkspaceActions: FC = ({ const { duplicateWorkspace, isDuplicationReady } = useWorkspaceDuplication(workspace); - const [isDownloadDialogOpen, setIsDownloadDialogOpen] = useState(false); - const { user } = useAuthenticated(); const isOwner = user.roles.find((role) => role.name === "owner") !== undefined; @@ -205,30 +198,8 @@ export const WorkspaceActions: FC = ({ Duplicate… - - setIsDownloadDialogOpen(true)}> - - Download logs… - - - - - - - Delete… - - - setIsDownloadDialogOpen(false)} - /> ); }; diff --git a/site/src/pages/WorkspacePage/WorkspaceDeleteDialog/index.ts b/site/src/pages/WorkspacePage/WorkspaceDeleteDialog/index.ts deleted file mode 100644 index cc0251b4a2ce0..0000000000000 --- a/site/src/pages/WorkspacePage/WorkspaceDeleteDialog/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./WorkspaceDeleteDialog"; diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 592ec64ec5b5b..f86b088217c45 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -109,6 +109,7 @@ export const WorkspaceReadyPage: FC = ({ latestVersion, }); + // Delete workspace const [isConfirmingDelete, setIsConfirmingDelete] = useState(false); const deleteWorkspaceMutation = useMutation( deleteWorkspace(workspace, queryClient), @@ -223,9 +224,6 @@ export const WorkspaceReadyPage: FC = ({ handleStop={() => { stopWorkspaceMutation.mutate({}); }} - handleDelete={() => { - setIsConfirmingDelete(true); - }} handleRestart={(buildParameters) => { setConfirmingRestart({ open: true, buildParameters }); }} @@ -258,20 +256,6 @@ export const WorkspaceReadyPage: FC = ({ timings={timingsQuery.data} /> - { - setIsConfirmingDelete(false); - }} - onConfirm={(orphan) => { - deleteWorkspaceMutation.mutate({ orphan }); - setIsConfirmingDelete(false); - }} - workspaceBuildDateStr={dayjs(workspace.created_at).fromNow()} - /> - { diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index f7fc892b278c4..31dac961b1ed0 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -41,7 +41,6 @@ export interface WorkspaceProps { handleStart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; handleStop: () => void; handleRestart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; - handleDelete: () => void; handleUpdate: () => void; handleCancel: () => void; handleSettings: () => void; @@ -63,7 +62,6 @@ export const WorkspaceTopbar: FC = ({ handleStart, handleStop, handleRestart, - handleDelete, handleUpdate, handleCancel, handleSettings, @@ -243,7 +241,6 @@ export const WorkspaceTopbar: FC = ({ handleStart={handleStart} handleStop={handleStop} handleRestart={handleRestart} - handleDelete={handleDelete} handleUpdate={handleUpdate} handleCancel={handleCancel} handleSettings={handleSettings} From f7e9ba9ac27d83340f09757d92983cc28a5470bb Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 12 May 2025 18:34:15 +0000 Subject: [PATCH 04/14] Refactor duplicate and clean up code --- .../WorkspaceMoreActions.tsx | 11 +-- site/src/pages/WorkspacePage/Workspace.tsx | 59 ++++++------- .../WorkspaceActions/WorkspaceActions.tsx | 85 ++++++------------- .../WorkspacePage/WorkspaceReadyPage.tsx | 45 +++------- .../pages/WorkspacePage/WorkspaceTopbar.tsx | 42 ++++----- 5 files changed, 84 insertions(+), 158 deletions(-) diff --git a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx index 500b17d6676a9..8a9dbe2a79d61 100644 --- a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx +++ b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx @@ -25,21 +25,18 @@ import { Link as RouterLink } from "react-router-dom"; import { ChangeWorkspaceVersionDialog } from "./ChangeWorkspaceVersionDialog"; import { WorkspaceDeleteDialog } from "./WorkspaceDeleteDialog"; import type { WorkspacePermissions } from "../permissions"; +import { useWorkspaceDuplication } from "pages/CreateWorkspacePage/useWorkspaceDuplication"; type WorkspaceMoreActionsProps = { workspace: Workspace; - isDuplicationReady: boolean; permissions: WorkspacePermissions; disabled?: boolean; - onDuplicate: () => void; }; export const WorkspaceMoreActions: FC = ({ workspace, disabled, permissions, - isDuplicationReady, - onDuplicate, }) => { const queryClient = useQueryClient(); @@ -58,6 +55,10 @@ export const WorkspaceMoreActions: FC = ({ deleteWorkspace(workspace, queryClient), ); + // Duplicate + const { duplicateWorkspace, isDuplicationReady } = + useWorkspaceDuplication(workspace); + return ( <> @@ -94,7 +95,7 @@ export const WorkspaceMoreActions: FC = ({ )} diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index bb7ca4ac98fa8..985190ac257e6 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -28,43 +28,33 @@ import type { WorkspacePermissions } from "../../modules/workspaces/permissions" import { resourceOptionValue, useResourcesNav } from "./useResourcesNav"; export interface WorkspaceProps { - handleStart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; - handleStop: () => void; - handleRestart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; - handleUpdate: () => void; - handleCancel: () => void; - handleSettings: () => void; - handleDormantActivate: () => void; - handleToggleFavorite: () => void; + workspace: TypesGen.Workspace; + template: TypesGen.Template; + permissions: WorkspacePermissions; isUpdating: boolean; isRestarting: boolean; - workspace: TypesGen.Workspace; hideSSHButton?: boolean; hideVSCodeDesktopButton?: boolean; buildInfo?: TypesGen.BuildInfoResponse; sshPrefix?: string; - template: TypesGen.Template; - canDebugMode: boolean; - handleRetry: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; - handleDebug: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; buildLogs?: TypesGen.ProvisionerJobLog[]; latestVersion?: TypesGen.TemplateVersion; - permissions: WorkspacePermissions; timings?: TypesGen.WorkspaceBuildTimings; + handleStart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; + handleStop: () => void; + handleRestart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; + handleUpdate: () => void; + handleCancel: () => void; + handleDormantActivate: () => void; + handleToggleFavorite: () => void; + handleRetry: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; + handleDebug: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; } /** * Workspace is the top-level component for viewing an individual workspace */ export const Workspace: FC = ({ - handleStart, - handleStop, - handleRestart, - handleUpdate, - handleCancel, - handleSettings, - handleDormantActivate, - handleToggleFavorite, workspace, isUpdating, isRestarting, @@ -73,13 +63,19 @@ export const Workspace: FC = ({ buildInfo, sshPrefix, template, - canDebugMode, - handleRetry, - handleDebug, buildLogs, latestVersion, permissions, timings, + handleStart, + handleStop, + handleRestart, + handleUpdate, + handleCancel, + handleDormantActivate, + handleToggleFavorite, + handleRetry, + handleDebug, }) => { const navigate = useNavigate(); const theme = useTheme(); @@ -136,23 +132,20 @@ export const Workspace: FC = ({ >
void; handleStart: (buildParameters?: WorkspaceBuildParameter[]) => void; handleStop: () => void; handleRestart: (buildParameters?: WorkspaceBuildParameter[]) => void; handleUpdate: () => void; handleCancel: () => void; - handleSettings: () => void; handleRetry: (buildParameters?: WorkspaceBuildParameter[]) => void; handleDebug: (buildParameters?: WorkspaceBuildParameter[]) => void; handleDormantActivate: () => void; - isUpdating: boolean; - isRestarting: boolean; - children?: ReactNode; - canChangeVersions: boolean; - canDebug: boolean; } export const WorkspaceActions: FC = ({ workspace, + isUpdating, + isRestarting, + permissions, handleToggleFavorite, handleStart, handleStop, handleRestart, handleUpdate, handleCancel, - handleSettings, handleRetry, handleDebug, handleDormantActivate, - isUpdating, - isRestarting, - canChangeVersions, - canDebug, }) => { - const { duplicateWorkspace, isDuplicationReady } = - useWorkspaceDuplication(workspace); - const { user } = useAuthenticated(); const isOwner = user.roles.find((role) => role.name === "owner") !== undefined; const { actions, canCancel, canAcceptJobs } = abilitiesByWorkspaceStatus( workspace, - { canDebug, isOwner }, + { canDebug: permissions.deploymentConfig, isOwner }, ); - const mustUpdate = mustUpdateWorkspace(workspace, canChangeVersions); - const tooltipText = getTooltipText(workspace, mustUpdate, canChangeVersions); + const mustUpdate = mustUpdateWorkspace( + workspace, + permissions.updateWorkspaceVersion, + ); + const tooltipText = getTooltipText( + workspace, + mustUpdate, + permissions.updateWorkspaceVersion, + ); // A mapping of button type to the corresponding React component const buttonMapping: Record = { @@ -170,36 +158,11 @@ export const WorkspaceActions: FC = ({ onToggle={handleToggleFavorite} /> - - - - - - - - - Settings - - - - - Duplicate… - - - +
); }; diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index f86b088217c45..4ded092ef92c7 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -1,13 +1,12 @@ -import { API, MissingBuildParameters } from "api/api"; +import { API } from "api/api"; import { getErrorMessage } from "api/errors"; import { buildInfo } from "api/queries/buildInfo"; -import { deploymentConfig, deploymentSSHConfig } from "api/queries/deployment"; -import { templateVersion, templateVersions } from "api/queries/templates"; +import { deploymentSSHConfig } from "api/queries/deployment"; +import { templateVersion } from "api/queries/templates"; import { workspaceBuildTimings } from "api/queries/workspaceBuilds"; import { activate, cancelBuild, - changeVersion, deleteWorkspace, startWorkspace, stopWorkspace, @@ -19,7 +18,6 @@ import { type ConfirmDialogProps, } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; import { displayError } from "components/GlobalSnackbar/utils"; -import dayjs from "dayjs"; import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata"; import { useWorkspaceBuildLogs } from "hooks/useWorkspaceBuildLogs"; import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility"; @@ -30,10 +28,8 @@ import { import { type FC, useEffect, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQuery, useQueryClient } from "react-query"; -import { useNavigate } from "react-router-dom"; import { pageTitle } from "utils/page"; import { Workspace } from "./Workspace"; -import { WorkspaceDeleteDialog } from "./WorkspaceDeleteDialog"; import type { WorkspacePermissions } from "modules/workspaces/permissions"; interface WorkspaceReadyPageProps { @@ -49,19 +45,8 @@ export const WorkspaceReadyPage: FC = ({ }) => { const { metadata } = useEmbeddedMetadata(); const buildInfoQuery = useQuery(buildInfo(metadata["build-info"])); - const navigate = useNavigate(); const queryClient = useQueryClient(); - const featureVisibility = useFeatureVisibility(); - if (workspace === undefined) { - throw Error("Workspace is undefined"); - } - - // Debug mode - const { data: deploymentValues } = useQuery({ - ...deploymentConfig(), - enabled: permissions.deploymentConfig, - }); // Build logs const shouldStreamBuildLogs = workspace.latest_build.status !== "running"; @@ -96,8 +81,7 @@ export const WorkspaceReadyPage: FC = ({ setFaviconTheme(isDark.matches ? "light" : "dark"); }, []); - // Versions - + // Active version const { data: latestVersion } = useQuery({ ...templateVersion(workspace.template_active_version_id), enabled: workspace.outdated, @@ -110,7 +94,6 @@ export const WorkspaceReadyPage: FC = ({ }); // Delete workspace - const [isConfirmingDelete, setIsConfirmingDelete] = useState(false); const deleteWorkspaceMutation = useMutation( deleteWorkspace(workspace, queryClient), ); @@ -218,6 +201,14 @@ export const WorkspaceReadyPage: FC = ({ isUpdating={workspaceUpdate.isUpdating} isRestarting={isRestarting} workspace={workspace} + latestVersion={latestVersion} + hideSSHButton={featureVisibility.browser_only} + hideVSCodeDesktopButton={featureVisibility.browser_only} + buildInfo={buildInfoQuery.data} + sshPrefix={sshPrefixQuery.data?.hostname_prefix} + template={template} + buildLogs={buildLogs} + timings={timingsQuery.data} handleStart={(buildParameters) => { startWorkspaceMutation.mutate({ buildParameters }); }} @@ -229,12 +220,8 @@ export const WorkspaceReadyPage: FC = ({ }} handleUpdate={workspaceUpdate.update} handleCancel={cancelBuildMutation.mutate} - handleSettings={() => navigate("settings")} handleRetry={handleRetry} handleDebug={handleDebug} - canDebugMode={ - deploymentValues?.config.enable_terraform_debug_mode ?? false - } handleDormantActivate={async () => { try { await activateWorkspaceMutation.mutateAsync(); @@ -246,14 +233,6 @@ export const WorkspaceReadyPage: FC = ({ handleToggleFavorite={() => { toggleFavoriteMutation.mutate(); }} - latestVersion={latestVersion} - hideSSHButton={featureVisibility.browser_only} - hideVSCodeDesktopButton={featureVisibility.browser_only} - buildInfo={buildInfoQuery.data} - sshPrefix={sshPrefixQuery.data?.hostname_prefix} - template={template} - buildLogs={buildLogs} - timings={timingsQuery.data} /> >; - export interface WorkspaceProps { + isUpdating: boolean; + isRestarting: boolean; + workspace: TypesGen.Workspace; + template: TypesGen.Template; + permissions: WorkspacePermissions; + latestVersion?: TypesGen.TemplateVersion; handleStart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; handleStop: () => void; handleRestart: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; handleUpdate: () => void; handleCancel: () => void; - handleSettings: () => void; handleDormantActivate: () => void; - isUpdating: boolean; - isRestarting: boolean; - workspace: TypesGen.Workspace; - canUpdateWorkspace: boolean; - canDebugMode: boolean; handleRetry: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; handleDebug: (buildParameters?: TypesGen.WorkspaceBuildParameter[]) => void; - template: TypesGen.Template; - permissions: WorkspacePermissions; - latestVersion?: TypesGen.TemplateVersion; handleToggleFavorite: () => void; } export const WorkspaceTopbar: FC = ({ + workspace, + template, + latestVersion, + permissions, + isUpdating, + isRestarting, handleStart, handleStop, handleRestart, handleUpdate, handleCancel, - handleSettings, handleDormantActivate, handleToggleFavorite, - workspace, - isUpdating, - isRestarting, - canUpdateWorkspace, - canDebugMode, handleRetry, handleDebug, - template, - latestVersion, - permissions, }) => { const { entitlements, organizations, showOrganizations } = useDashboard(); const getLink = useLinks(); @@ -224,7 +216,7 @@ export const WorkspaceTopbar: FC = ({ = ({ )} From a3c4908c51bf84b0582bdaa6dd8176f66996d156 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 12 May 2025 18:45:34 +0000 Subject: [PATCH 05/14] Move components and fix build errors --- .../WorkspaceMoreActions}/DownloadLogsDialog.stories.tsx | 2 +- .../WorkspaceMoreActions}/DownloadLogsDialog.tsx | 5 +++-- .../WorkspaceMoreActions}/UpdateBuildParametersDialog.tsx | 0 .../WorkspaceMoreActions/WorkspaceMoreActions.tsx | 6 +++--- .../useWorkspaceDuplication.test.tsx | 2 +- .../WorkspaceMoreActions}/useWorkspaceDuplication.ts | 2 +- site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx | 2 +- site/src/pages/WorkspacePage/Workspace.stories.tsx | 5 +++-- .../WorkspaceActions/WorkspaceActions.stories.tsx | 8 +++++++- .../WorkspaceNotifications.stories.tsx | 8 +++++--- site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx | 8 +++++++- .../WorkspaceParametersPage/WorkspaceParametersPage.tsx | 5 ++--- 12 files changed, 34 insertions(+), 19 deletions(-) rename site/src/{pages/WorkspacePage/WorkspaceActions => modules/workspaces/WorkspaceMoreActions}/DownloadLogsDialog.stories.tsx (97%) rename site/src/{pages/WorkspacePage/WorkspaceActions => modules/workspaces/WorkspaceMoreActions}/DownloadLogsDialog.tsx (98%) rename site/src/{pages/WorkspacePage => modules/workspaces/WorkspaceMoreActions}/UpdateBuildParametersDialog.tsx (100%) rename site/src/{pages/CreateWorkspacePage => modules/workspaces/WorkspaceMoreActions}/useWorkspaceDuplication.test.tsx (97%) rename site/src/{pages/CreateWorkspacePage => modules/workspaces/WorkspaceMoreActions}/useWorkspaceDuplication.ts (96%) diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.stories.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/DownloadLogsDialog.stories.tsx similarity index 97% rename from site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.stories.tsx rename to site/src/modules/workspaces/WorkspaceMoreActions/DownloadLogsDialog.stories.tsx index 14bd1b4a6d6b7..ad925798ac8d3 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.stories.tsx +++ b/site/src/modules/workspaces/WorkspaceMoreActions/DownloadLogsDialog.stories.tsx @@ -6,7 +6,7 @@ import { withDesktopViewport } from "testHelpers/storybook"; import { DownloadLogsDialog } from "./DownloadLogsDialog"; const meta: Meta = { - title: "pages/WorkspacePage/DownloadLogsDialog", + title: "modules/workspaces/DownloadLogsDialog", component: DownloadLogsDialog, args: { open: true, diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/DownloadLogsDialog.tsx similarity index 98% rename from site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx rename to site/src/modules/workspaces/WorkspaceMoreActions/DownloadLogsDialog.tsx index 0beeb795a09b6..863bcb07061da 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog.tsx +++ b/site/src/modules/workspaces/WorkspaceMoreActions/DownloadLogsDialog.tsx @@ -221,8 +221,9 @@ const DownloadingItem: FC = ({ file, giveUpTimeMs }) => { function humanBlobSize(size: number) { const BLOB_SIZE_UNITS = ["B", "KB", "MB", "GB", "TB"] as const; let i = 0; - while (size > 1024 && i < BLOB_SIZE_UNITS.length) { - size /= 1024; + let sizeIterator = size; + while (sizeIterator > 1024 && i < BLOB_SIZE_UNITS.length) { + sizeIterator /= 1024; i++; } diff --git a/site/src/pages/WorkspacePage/UpdateBuildParametersDialog.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/UpdateBuildParametersDialog.tsx similarity index 100% rename from site/src/pages/WorkspacePage/UpdateBuildParametersDialog.tsx rename to site/src/modules/workspaces/WorkspaceMoreActions/UpdateBuildParametersDialog.tsx diff --git a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx index 8a9dbe2a79d61..7ac8b33936a13 100644 --- a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx +++ b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx @@ -17,15 +17,15 @@ import { CopyIcon, DownloadIcon, } from "lucide-react"; -import { UpdateBuildParametersDialog } from "pages/WorkspacePage/UpdateBuildParametersDialog"; -import { DownloadLogsDialog } from "pages/WorkspacePage/WorkspaceActions/DownloadLogsDialog"; +import { UpdateBuildParametersDialog } from "./UpdateBuildParametersDialog"; +import { DownloadLogsDialog } from "./DownloadLogsDialog"; import { useState, type FC } from "react"; import { useMutation, useQueryClient } from "react-query"; import { Link as RouterLink } from "react-router-dom"; import { ChangeWorkspaceVersionDialog } from "./ChangeWorkspaceVersionDialog"; import { WorkspaceDeleteDialog } from "./WorkspaceDeleteDialog"; import type { WorkspacePermissions } from "../permissions"; -import { useWorkspaceDuplication } from "pages/CreateWorkspacePage/useWorkspaceDuplication"; +import { useWorkspaceDuplication } from "./useWorkspaceDuplication"; type WorkspaceMoreActionsProps = { workspace: Workspace; diff --git a/site/src/pages/CreateWorkspacePage/useWorkspaceDuplication.test.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/useWorkspaceDuplication.test.tsx similarity index 97% rename from site/src/pages/CreateWorkspacePage/useWorkspaceDuplication.test.tsx rename to site/src/modules/workspaces/WorkspaceMoreActions/useWorkspaceDuplication.test.tsx index ce2fead417c03..8e06e10136f92 100644 --- a/site/src/pages/CreateWorkspacePage/useWorkspaceDuplication.test.tsx +++ b/site/src/modules/workspaces/WorkspaceMoreActions/useWorkspaceDuplication.test.tsx @@ -5,7 +5,7 @@ import { type GetLocationSnapshot, renderHookWithAuth, } from "testHelpers/hooks"; -import CreateWorkspacePage from "./CreateWorkspacePage"; +import CreateWorkspacePage from "../../../pages/CreateWorkspacePage/CreateWorkspacePage"; import { useWorkspaceDuplication } from "./useWorkspaceDuplication"; function render(workspace?: Workspace) { diff --git a/site/src/pages/CreateWorkspacePage/useWorkspaceDuplication.ts b/site/src/modules/workspaces/WorkspaceMoreActions/useWorkspaceDuplication.ts similarity index 96% rename from site/src/pages/CreateWorkspacePage/useWorkspaceDuplication.ts rename to site/src/modules/workspaces/WorkspaceMoreActions/useWorkspaceDuplication.ts index b98434a0b51f9..cde6707be6f26 100644 --- a/site/src/pages/CreateWorkspacePage/useWorkspaceDuplication.ts +++ b/site/src/modules/workspaces/WorkspaceMoreActions/useWorkspaceDuplication.ts @@ -4,7 +4,7 @@ import { linkToTemplate, useLinks } from "modules/navigation"; import { useCallback } from "react"; import { useQuery } from "react-query"; import { useNavigate } from "react-router-dom"; -import type { CreateWorkspaceMode } from "./CreateWorkspacePage"; +import type { CreateWorkspaceMode } from "../../../pages/CreateWorkspacePage/CreateWorkspacePage"; function getDuplicationUrlParams( workspaceParams: readonly WorkspaceBuildParameter[], diff --git a/site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx b/site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx index 741bc12a6539b..0b4e53cedfb36 100644 --- a/site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx +++ b/site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx @@ -8,7 +8,7 @@ import type { } from "api/typesGenerated"; import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; import { MemoizedInlineMarkdown } from "components/Markdown/Markdown"; -import { UpdateBuildParametersDialog } from "pages/WorkspacePage/UpdateBuildParametersDialog"; +import { UpdateBuildParametersDialog } from "modules/workspaces/WorkspaceMoreActions/UpdateBuildParametersDialog"; import { type FC, useState } from "react"; import { useMutation, useQueryClient } from "react-query"; diff --git a/site/src/pages/WorkspacePage/Workspace.stories.tsx b/site/src/pages/WorkspacePage/Workspace.stories.tsx index 395f1bed99acb..bd37c34477148 100644 --- a/site/src/pages/WorkspacePage/Workspace.stories.tsx +++ b/site/src/pages/WorkspacePage/Workspace.stories.tsx @@ -21,8 +21,9 @@ const createTimestamp = ( const permissions: WorkspacePermissions = { readWorkspace: true, updateWorkspace: true, - updateTemplate: true, - viewDeploymentConfig: true, + updateWorkspaceVersion: true, + deploymentConfig: true, + deleteFailedWorkspace: true, }; const meta: Meta = { diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx index a67690f929b5f..5fa238eae7ef0 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx @@ -157,7 +157,13 @@ export const Failed: Story = { export const FailedWithDebug: Story = { args: { workspace: Mocks.MockFailedWorkspace, - canDebug: true, + permissions: { + deploymentConfig: true, + deleteFailedWorkspace: true, + readWorkspace: true, + updateWorkspace: true, + updateWorkspaceVersion: true, + }, }, }; diff --git a/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.stories.tsx b/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.stories.tsx index 6f02d925f6485..9a240fa0cbdc0 100644 --- a/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.stories.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.stories.tsx @@ -10,12 +10,14 @@ import { } from "testHelpers/entities"; import { withDashboardProvider } from "testHelpers/storybook"; import { WorkspaceNotifications } from "./WorkspaceNotifications"; +import type { WorkspacePermissions } from "modules/workspaces/permissions"; -const defaultPermissions = { +const defaultPermissions: WorkspacePermissions = { readWorkspace: true, - updateTemplate: true, + updateWorkspaceVersion: true, updateWorkspace: true, - viewDeploymentConfig: true, + deploymentConfig: true, + deleteFailedWorkspace: true, }; const meta: Meta = { diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx index d9b093513ed8f..63eb8bba27218 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx @@ -33,7 +33,13 @@ const meta: Meta = { workspace: baseWorkspace, template: MockTemplate, latestVersion: MockTemplateVersion, - canUpdateWorkspace: true, + permissions: { + readWorkspace: true, + updateWorkspaceVersion: true, + updateWorkspace: true, + deploymentConfig: true, + deleteFailedWorkspace: true, + }, }, parameters: { layout: "fullscreen", diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx index 8377d7266e02e..38136fb7d233b 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx @@ -50,14 +50,13 @@ const WorkspaceParametersPage: FC = () => { const template = templateQuery.data; // Permissions - const checks = - workspace && template ? workspaceChecks(workspace, template) : {}; + const checks = workspace && template ? workspaceChecks(workspace) : {}; const permissionsQuery = useQuery({ ...checkAuthorization({ checks }), enabled: workspace !== undefined && template !== undefined, }); const permissions = permissionsQuery.data as WorkspacePermissions | undefined; - const canChangeVersions = Boolean(permissions?.updateTemplate); + const canChangeVersions = Boolean(permissions?.updateWorkspaceVersion); return ( <> From d5ae7e148fda1970be3e01592982b3afe1c4bd07 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 12 May 2025 18:49:35 +0000 Subject: [PATCH 06/14] Minor visual fixes --- site/src/components/DropdownMenu/DropdownMenu.tsx | 2 +- .../WorkspaceMoreActions/ChangeWorkspaceVersionDialog.tsx | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/site/src/components/DropdownMenu/DropdownMenu.tsx b/site/src/components/DropdownMenu/DropdownMenu.tsx index e56fd7cbe4343..8d9fb12d774a3 100644 --- a/site/src/components/DropdownMenu/DropdownMenu.tsx +++ b/site/src/components/DropdownMenu/DropdownMenu.tsx @@ -111,7 +111,7 @@ export const DropdownMenuItem = forwardRef< [ "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-2 text-sm text-content-secondary font-medium outline-none transition-colors", "focus:bg-surface-secondary focus:text-content-primary data-[disabled]:pointer-events-none data-[disabled]:opacity-50", - "[&>svg]:size-4 [&>svg]:shrink-0", + "[&>svg]:size-4 [&>svg]:shrink-0 no-underline", inset && "pl-8", ], className, diff --git a/site/src/modules/workspaces/WorkspaceMoreActions/ChangeWorkspaceVersionDialog.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/ChangeWorkspaceVersionDialog.tsx index 2a55b87a212fa..58e3e9d67a68e 100644 --- a/site/src/modules/workspaces/WorkspaceMoreActions/ChangeWorkspaceVersionDialog.tsx +++ b/site/src/modules/workspaces/WorkspaceMoreActions/ChangeWorkspaceVersionDialog.tsx @@ -29,7 +29,10 @@ export type ChangeWorkspaceVersionDialogProps = DialogProps & { export const ChangeWorkspaceVersionDialog: FC< ChangeWorkspaceVersionDialogProps > = ({ workspace, onClose, onConfirm, ...dialogProps }) => { - const { data: versions } = useQuery(templateVersions(workspace.template_id)); + const { data: versions } = useQuery({ + ...templateVersions(workspace.template_id), + select: (data) => data.reverse(), + }); const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false); const activeVersion = versions?.find( (v) => workspace.template_active_version_id === v.id, From 0c65d4f1c5df4f8272f61d1163d75caa4e9af6a7 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 12 May 2025 19:34:00 +0000 Subject: [PATCH 07/14] Fix debug check --- site/src/api/queries/deployment.ts | 1 + .../WorkspaceActions/WorkspaceActions.tsx | 13 ++++++++++--- site/src/pages/WorkspacePage/WorkspacePage.test.tsx | 12 ++++++++---- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/site/src/api/queries/deployment.ts b/site/src/api/queries/deployment.ts index 999dd2ee4cbd5..433b0054fc5b6 100644 --- a/site/src/api/queries/deployment.ts +++ b/site/src/api/queries/deployment.ts @@ -6,6 +6,7 @@ export const deploymentConfig = () => { return { queryKey: deploymentConfigQueryKey, queryFn: API.getDeploymentConfig, + staleTime: Number.POSITIVE_INFINITY, }; }; diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx index 48f68bab6e011..c911e59430279 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx @@ -22,6 +22,8 @@ import { DebugButton } from "./DebugButton"; import { RetryButton } from "./RetryButton"; import { WorkspaceMoreActions } from "modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions"; import type { WorkspacePermissions } from "modules/workspaces/permissions"; +import { useQuery } from "react-query"; +import { deploymentConfig } from "api/queries/deployment"; export interface WorkspaceActionsProps { workspace: Workspace; @@ -55,11 +57,16 @@ export const WorkspaceActions: FC = ({ handleDormantActivate, }) => { const { user } = useAuthenticated(); - const isOwner = - user.roles.find((role) => role.name === "owner") !== undefined; + const { data: deployment } = useQuery({ + ...deploymentConfig(), + enabled: permissions.deploymentConfig, + }); const { actions, canCancel, canAcceptJobs } = abilitiesByWorkspaceStatus( workspace, - { canDebug: permissions.deploymentConfig, isOwner }, + { + canDebug: !!deployment?.config.enable_terraform_debug_mode, + isOwner: !!user.roles.find((role) => role.name === "owner"), + }, ); const mustUpdate = mustUpdateWorkspace( diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index a9f78f3610983..99048ee7b95af 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -33,6 +33,7 @@ import { } from "testHelpers/renderHelpers"; import { server } from "testHelpers/server"; import WorkspacePage from "./WorkspacePage"; +import type { WorkspacePermissions } from "modules/workspaces/permissions"; const { API, MissingBuildParameters } = apiModule; @@ -126,11 +127,14 @@ describe("WorkspacePage", () => { // set permissions server.use( http.post("/api/v2/authcheck", async () => { - return HttpResponse.json({ - updateTemplates: true, + const permissions: WorkspacePermissions = { + deleteFailedWorkspace: true, + deploymentConfig: true, + readWorkspace: true, updateWorkspace: true, - updateTemplate: true, - }); + updateWorkspaceVersion: true, + }; + return HttpResponse.json(permissions); }), ); From 3b5f20bfc34c6894bfb11ad828d4ee3dd61c640a Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 12 May 2025 19:46:04 +0000 Subject: [PATCH 08/14] Add more actions to workspaces row --- .../WorkspaceMoreActions.tsx | 17 +++++++++++------ .../WorkspaceActions/WorkspaceActions.tsx | 6 +----- .../pages/WorkspacesPage/WorkspacesTable.tsx | 19 ++++++++----------- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx index 7ac8b33936a13..9cb07bd2c96f7 100644 --- a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx +++ b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx @@ -1,5 +1,9 @@ import { MissingBuildParameters } from "api/api"; -import { changeVersion, deleteWorkspace } from "api/queries/workspaces"; +import { + changeVersion, + deleteWorkspace, + workspacePermissions, +} from "api/queries/workspaces"; import type { Workspace } from "api/typesGenerated"; import { Button } from "components/Button/Button"; import { @@ -20,7 +24,7 @@ import { import { UpdateBuildParametersDialog } from "./UpdateBuildParametersDialog"; import { DownloadLogsDialog } from "./DownloadLogsDialog"; import { useState, type FC } from "react"; -import { useMutation, useQueryClient } from "react-query"; +import { useMutation, useQuery, useQueryClient } from "react-query"; import { Link as RouterLink } from "react-router-dom"; import { ChangeWorkspaceVersionDialog } from "./ChangeWorkspaceVersionDialog"; import { WorkspaceDeleteDialog } from "./WorkspaceDeleteDialog"; @@ -29,17 +33,18 @@ import { useWorkspaceDuplication } from "./useWorkspaceDuplication"; type WorkspaceMoreActionsProps = { workspace: Workspace; - permissions: WorkspacePermissions; disabled?: boolean; }; export const WorkspaceMoreActions: FC = ({ workspace, disabled, - permissions, }) => { const queryClient = useQueryClient(); + // Permissions + const { data: permissions } = useQuery(workspacePermissions(workspace)); + // Download logs const [isDownloadDialogOpen, setIsDownloadDialogOpen] = useState(false); @@ -83,7 +88,7 @@ export const WorkspaceMoreActions: FC = ({
- {permissions.updateWorkspaceVersion && ( + {permissions?.updateWorkspaceVersion && ( { setChangeVersionDialogOpen(true); @@ -162,7 +167,7 @@ export const WorkspaceMoreActions: FC = ({ { setIsConfirmingDelete(false); diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx index c911e59430279..4fadf8097a8e6 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx @@ -165,11 +165,7 @@ export const WorkspaceActions: FC = ({ onToggle={handleToggleFavorite} /> - + ); }; diff --git a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx index 4ec1156b7fcd5..09da06cac24d9 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx @@ -91,6 +91,7 @@ import { lastUsedMessage, } from "utils/workspace"; import { WorkspacesEmpty } from "./WorkspacesEmpty"; +import { WorkspaceMoreActions } from "modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions"; dayjs.extend(relativeTime); @@ -185,7 +186,6 @@ export const WorkspacesTable: FC = ({ Template Status - @@ -303,11 +303,6 @@ export const WorkspacesTable: FC = ({ onActionSuccess={onActionSuccess} onActionError={onActionError} /> - -
- -
-
); })} @@ -385,11 +380,6 @@ const TableLoader: FC = ({ canCheckWorkspaces }) => { - -
- -
-
); @@ -585,6 +575,11 @@ const WorkspaceActionsCell: FC = ({ )} + + ); @@ -731,6 +726,8 @@ const WorkspaceApps: FC = ({ workspace }) => { ); } + buttons.push(); + return buttons; }; From 207094afa5dcfaef3793eb52e72ffe46d81b1a9d Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 12 May 2025 19:53:12 +0000 Subject: [PATCH 09/14] Minor fixes --- .../WorkspaceMoreActions.tsx | 4 ++- .../pages/WorkspacesPage/WorkspacesTable.tsx | 27 ++++++++++--------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx index 9cb07bd2c96f7..3bb1a34875fb4 100644 --- a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx +++ b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx @@ -82,7 +82,9 @@ export const WorkspaceMoreActions: FC = ({ - + Settings diff --git a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx index 09da06cac24d9..2fdcc89e64941 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx @@ -52,7 +52,7 @@ import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; import { useAuthenticated } from "hooks"; import { useClickableTableRow } from "hooks/useClickableTableRow"; -import { ChevronRightIcon } from "lucide-react"; +import { ChevronRightIcon, EllipsisVertical } from "lucide-react"; import { BanIcon, PlayIcon, @@ -377,8 +377,13 @@ const TableLoader: FC = ({ canCheckWorkspaces }) => { - - + +
+ + +
@@ -527,7 +532,12 @@ const WorkspaceActionsCell: FC = ({ }; return ( - + { + // Prevent the click in the actions to trigger the row click + e.stopPropagation(); + }} + >
{workspace.latest_build.status === "running" && ( @@ -601,14 +611,7 @@ const PrimaryAction: FC = ({ - From c79952bc48f68ff0d5a7fc36ca8719cefda133f4 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 12 May 2025 22:07:51 +0000 Subject: [PATCH 10/14] Run FMT --- site/src/api/queries/workspaces.ts | 10 +++++----- .../ChangeWorkspaceVersionDialog.stories.tsx | 3 +-- .../WorkspaceDeleteDialog.stories.tsx | 2 +- .../WorkspaceMoreActions/WorkspaceMoreActions.tsx | 13 ++++++------- site/src/pages/WorkspacePage/Workspace.stories.tsx | 2 +- site/src/pages/WorkspacePage/Workspace.tsx | 2 +- .../WorkspaceActions/WorkspaceActions.tsx | 8 ++++---- .../WorkspaceNotifications.stories.tsx | 2 +- site/src/pages/WorkspacePage/WorkspacePage.test.tsx | 2 +- site/src/pages/WorkspacePage/WorkspacePage.tsx | 1 - site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx | 2 +- site/src/pages/WorkspacePage/WorkspaceTopbar.tsx | 2 +- site/src/pages/WorkspacesPage/WorkspacesTable.tsx | 4 ++-- 13 files changed, 25 insertions(+), 28 deletions(-) diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index 668fe77092fae..4f4b9b80cc8f9 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -11,19 +11,19 @@ import type { WorkspacesResponse, } from "api/typesGenerated"; import type { Dayjs } from "dayjs"; +import { + type WorkspacePermissions, + workspaceChecks, +} from "modules/workspaces/permissions"; import type { ConnectionStatus } from "pages/TerminalPage/types"; import type { QueryClient, QueryOptions, UseMutationOptions, } from "react-query"; +import { checkAuthorization } from "./authCheck"; import { disabledRefetchOptions } from "./util"; import { workspaceBuildsKey } from "./workspaceBuilds"; -import { - workspaceChecks, - type WorkspacePermissions, -} from "modules/workspaces/permissions"; -import { checkAuthorization } from "./authCheck"; export const workspaceByOwnerAndNameKey = (owner: string, name: string) => [ "workspace", diff --git a/site/src/modules/workspaces/WorkspaceMoreActions/ChangeWorkspaceVersionDialog.stories.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/ChangeWorkspaceVersionDialog.stories.tsx index 6c85fa7d571f0..91bd4f49dbb2f 100644 --- a/site/src/modules/workspaces/WorkspaceMoreActions/ChangeWorkspaceVersionDialog.stories.tsx +++ b/site/src/modules/workspaces/WorkspaceMoreActions/ChangeWorkspaceVersionDialog.stories.tsx @@ -1,12 +1,11 @@ import type { Meta, StoryObj } from "@storybook/react"; +import { templateVersionsQueryKey } from "api/queries/templates"; import { - MockTemplate, MockTemplateVersion, MockTemplateVersionWithMarkdownMessage, MockWorkspace, } from "testHelpers/entities"; import { ChangeWorkspaceVersionDialog } from "./ChangeWorkspaceVersionDialog"; -import { templateVersionsQueryKey } from "api/queries/templates"; const noMessage = { ...MockTemplateVersion, diff --git a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceDeleteDialog.stories.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceDeleteDialog.stories.tsx index e6aaa28ee4247..e7b168e57e973 100644 --- a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceDeleteDialog.stories.tsx +++ b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceDeleteDialog.stories.tsx @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { MockFailedWorkspace, MockWorkspace } from "testHelpers/entities"; -import { WorkspaceDeleteDialog } from "./WorkspaceDeleteDialog"; import { daysAgo } from "utils/time"; +import { WorkspaceDeleteDialog } from "./WorkspaceDeleteDialog"; const meta: Meta = { title: "modules/workspaces/WorkspaceDeleteDialog", diff --git a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx index 3bb1a34875fb4..3bc6a471efb91 100644 --- a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx +++ b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx @@ -14,21 +14,20 @@ import { DropdownMenuTrigger, } from "components/DropdownMenu/DropdownMenu"; import { + CopyIcon, + DownloadIcon, EllipsisVertical, - SettingsIcon, HistoryIcon, + SettingsIcon, TrashIcon, - CopyIcon, - DownloadIcon, } from "lucide-react"; -import { UpdateBuildParametersDialog } from "./UpdateBuildParametersDialog"; -import { DownloadLogsDialog } from "./DownloadLogsDialog"; -import { useState, type FC } from "react"; +import { type FC, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "react-query"; import { Link as RouterLink } from "react-router-dom"; import { ChangeWorkspaceVersionDialog } from "./ChangeWorkspaceVersionDialog"; +import { DownloadLogsDialog } from "./DownloadLogsDialog"; +import { UpdateBuildParametersDialog } from "./UpdateBuildParametersDialog"; import { WorkspaceDeleteDialog } from "./WorkspaceDeleteDialog"; -import type { WorkspacePermissions } from "../permissions"; import { useWorkspaceDuplication } from "./useWorkspaceDuplication"; type WorkspaceMoreActionsProps = { diff --git a/site/src/pages/WorkspacePage/Workspace.stories.tsx b/site/src/pages/WorkspacePage/Workspace.stories.tsx index bd37c34477148..209fe72b944e7 100644 --- a/site/src/pages/WorkspacePage/Workspace.stories.tsx +++ b/site/src/pages/WorkspacePage/Workspace.stories.tsx @@ -4,8 +4,8 @@ import type { ProvisionerJobLog } from "api/typesGenerated"; import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext"; import * as Mocks from "testHelpers/entities"; import { withAuthProvider, withDashboardProvider } from "testHelpers/storybook"; -import { Workspace } from "./Workspace"; import type { WorkspacePermissions } from "../../modules/workspaces/permissions"; +import { Workspace } from "./Workspace"; // Helper function to create timestamps easily - Copied from AppStatuses.stories.tsx const createTimestamp = ( diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index 985190ac257e6..aa82a6ddefba4 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -13,6 +13,7 @@ import { AgentRow } from "modules/resources/AgentRow"; import { WorkspaceTimings } from "modules/workspaces/WorkspaceTiming/WorkspaceTimings"; import { type FC, useMemo } from "react"; import { useNavigate } from "react-router-dom"; +import type { WorkspacePermissions } from "../../modules/workspaces/permissions"; import { AppStatuses } from "./AppStatuses"; import { HistorySidebar } from "./HistorySidebar"; import { ResourceMetadata } from "./ResourceMetadata"; @@ -24,7 +25,6 @@ import { } from "./WorkspaceBuildProgress"; import { WorkspaceDeletedBanner } from "./WorkspaceDeletedBanner"; import { WorkspaceTopbar } from "./WorkspaceTopbar"; -import type { WorkspacePermissions } from "../../modules/workspaces/permissions"; import { resourceOptionValue, useResourcesNav } from "./useResourcesNav"; export interface WorkspaceProps { diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx index 4fadf8097a8e6..98a0a36fd9ff6 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx @@ -1,10 +1,14 @@ +import { deploymentConfig } from "api/queries/deployment"; import type { Workspace, WorkspaceBuildParameter } from "api/typesGenerated"; import { useAuthenticated } from "hooks/useAuthenticated"; +import { WorkspaceMoreActions } from "modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions"; import { type ActionType, abilitiesByWorkspaceStatus, } from "modules/workspaces/actions"; +import type { WorkspacePermissions } from "modules/workspaces/permissions"; import { type FC, Fragment, type ReactNode } from "react"; +import { useQuery } from "react-query"; import { mustUpdateWorkspace } from "utils/workspace"; import { ActivateButton, @@ -20,10 +24,6 @@ import { } from "./Buttons"; import { DebugButton } from "./DebugButton"; import { RetryButton } from "./RetryButton"; -import { WorkspaceMoreActions } from "modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions"; -import type { WorkspacePermissions } from "modules/workspaces/permissions"; -import { useQuery } from "react-query"; -import { deploymentConfig } from "api/queries/deployment"; export interface WorkspaceActionsProps { workspace: Workspace; diff --git a/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.stories.tsx b/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.stories.tsx index 9a240fa0cbdc0..a35771971b329 100644 --- a/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.stories.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceNotifications/WorkspaceNotifications.stories.tsx @@ -1,6 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { expect, userEvent, waitFor, within } from "@storybook/test"; import { getWorkspaceResolveAutostartQueryKey } from "api/queries/workspaceQuota"; +import type { WorkspacePermissions } from "modules/workspaces/permissions"; import { MockOutdatedWorkspace, MockTemplate, @@ -10,7 +11,6 @@ import { } from "testHelpers/entities"; import { withDashboardProvider } from "testHelpers/storybook"; import { WorkspaceNotifications } from "./WorkspaceNotifications"; -import type { WorkspacePermissions } from "modules/workspaces/permissions"; const defaultPermissions: WorkspacePermissions = { readWorkspace: true, diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index 99048ee7b95af..7489be8772ee4 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -7,6 +7,7 @@ import { DashboardContext, type DashboardProvider, } from "modules/dashboard/DashboardProvider"; +import type { WorkspacePermissions } from "modules/workspaces/permissions"; import { http, HttpResponse } from "msw"; import type { FC } from "react"; import { type Location, useLocation } from "react-router-dom"; @@ -33,7 +34,6 @@ import { } from "testHelpers/renderHelpers"; import { server } from "testHelpers/server"; import WorkspacePage from "./WorkspacePage"; -import type { WorkspacePermissions } from "modules/workspaces/permissions"; const { API, MissingBuildParameters } = apiModule; diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index aa32c06e264f7..f4b4af024d06e 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -1,5 +1,4 @@ import { watchWorkspace } from "api/api"; -import { checkAuthorization } from "api/queries/authCheck"; import { template as templateQueryOptions } from "api/queries/templates"; import { workspaceBuildsKey } from "api/queries/workspaceBuilds"; import { diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 4ded092ef92c7..5de4eb6b490f7 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -25,12 +25,12 @@ import { WorkspaceUpdateDialogs, useWorkspaceUpdate, } from "modules/workspaces/WorkspaceUpdateDialogs"; +import type { WorkspacePermissions } from "modules/workspaces/permissions"; import { type FC, useEffect, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQuery, useQueryClient } from "react-query"; import { pageTitle } from "utils/page"; import { Workspace } from "./Workspace"; -import type { WorkspacePermissions } from "modules/workspaces/permissions"; interface WorkspaceReadyPageProps { template: TypesGen.Template; diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index aae0682e8795d..21e14890d400f 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -25,10 +25,10 @@ import type { FC } from "react"; import { useQuery } from "react-query"; import { Link as RouterLink } from "react-router-dom"; import { displayDormantDeletion } from "utils/dormant"; +import type { WorkspacePermissions } from "../../modules/workspaces/permissions"; import { WorkspaceActions } from "./WorkspaceActions/WorkspaceActions"; import { WorkspaceNotifications } from "./WorkspaceNotifications/WorkspaceNotifications"; import { WorkspaceScheduleControls } from "./WorkspaceScheduleControls"; -import type { WorkspacePermissions } from "../../modules/workspaces/permissions"; export type WorkspaceError = | "getBuildsError" diff --git a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx index 2fdcc89e64941..b5453e424dfd7 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx @@ -52,7 +52,7 @@ import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; import { useAuthenticated } from "hooks"; import { useClickableTableRow } from "hooks/useClickableTableRow"; -import { ChevronRightIcon, EllipsisVertical } from "lucide-react"; +import { EllipsisVertical } from "lucide-react"; import { BanIcon, PlayIcon, @@ -68,6 +68,7 @@ import { useAppLink } from "modules/apps/useAppLink"; import { useDashboard } from "modules/dashboard/useDashboard"; import { WorkspaceAppStatus } from "modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus"; import { WorkspaceDormantBadge } from "modules/workspaces/WorkspaceDormantBadge/WorkspaceDormantBadge"; +import { WorkspaceMoreActions } from "modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions"; import { WorkspaceOutdatedTooltip } from "modules/workspaces/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip"; import { WorkspaceUpdateDialogs, @@ -91,7 +92,6 @@ import { lastUsedMessage, } from "utils/workspace"; import { WorkspacesEmpty } from "./WorkspacesEmpty"; -import { WorkspaceMoreActions } from "modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions"; dayjs.extend(relativeTime); From be967dd75c7e0043d0961308a083c8d369a7dfbe Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 12 May 2025 22:18:59 +0000 Subject: [PATCH 11/14] Fix lint and tests --- site/src/pages/WorkspacePage/WorkspaceTopbar.tsx | 5 ----- .../pages/WorkspacesPage/WorkspacesPage.test.tsx | 14 +++++++------- .../pages/WorkspacesPage/WorkspacesPageView.tsx | 2 +- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index 21e14890d400f..32908156c5b5c 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -30,11 +30,6 @@ import { WorkspaceActions } from "./WorkspaceActions/WorkspaceActions"; import { WorkspaceNotifications } from "./WorkspaceNotifications/WorkspaceNotifications"; import { WorkspaceScheduleControls } from "./WorkspaceScheduleControls"; -export type WorkspaceError = - | "getBuildsError" - | "buildError" - | "cancellationError"; - export interface WorkspaceProps { isUpdating: boolean; isRestarting: boolean; diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx index b1ad1d887e53c..c8577f191d47e 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx @@ -68,7 +68,7 @@ describe("WorkspacesPage", () => { await user.click(getWorkspaceCheckbox(workspaces[0])); await user.click(getWorkspaceCheckbox(workspaces[1])); - await user.click(screen.getByRole("button", { name: /actions/i })); + await user.click(screen.getByRole("button", { name: /bulk actions/i })); const deleteButton = await screen.findByText(/delete/i); await user.click(deleteButton); @@ -106,7 +106,7 @@ describe("WorkspacesPage", () => { await user.click(getWorkspaceCheckbox(workspace)); } - await user.click(screen.getByRole("button", { name: /actions/i })); + await user.click(screen.getByRole("button", { name: /bulk actions/i })); const updateButton = await screen.findByText(/update/i); await user.click(updateButton); @@ -145,7 +145,7 @@ describe("WorkspacesPage", () => { await user.click(getWorkspaceCheckbox(workspace)); } - await user.click(screen.getByRole("button", { name: /actions/i })); + await user.click(screen.getByRole("button", { name: /bulk actions/i })); const updateButton = await screen.findByText(/update/i); await user.click(updateButton); @@ -183,7 +183,7 @@ describe("WorkspacesPage", () => { await user.click(getWorkspaceCheckbox(workspace)); } - await user.click(screen.getByRole("button", { name: /actions/i })); + await user.click(screen.getByRole("button", { name: /bulk actions/i })); const updateButton = await screen.findByText(/update/i); await user.click(updateButton); @@ -223,7 +223,7 @@ describe("WorkspacesPage", () => { await user.click(getWorkspaceCheckbox(workspace)); } - await user.click(screen.getByRole("button", { name: /actions/i })); + await user.click(screen.getByRole("button", { name: /bulk actions/i })); const updateButton = await screen.findByText(/update/i); await user.click(updateButton); @@ -263,7 +263,7 @@ describe("WorkspacesPage", () => { await user.click(getWorkspaceCheckbox(workspaces[0])); await user.click(getWorkspaceCheckbox(workspaces[1])); - await user.click(screen.getByRole("button", { name: /actions/i })); + await user.click(screen.getByRole("button", { name: /bulk actions/i })); const stopButton = await screen.findByRole("menuitem", { name: /stop/i }); await user.click(stopButton); @@ -290,7 +290,7 @@ describe("WorkspacesPage", () => { await user.click(getWorkspaceCheckbox(workspaces[0])); await user.click(getWorkspaceCheckbox(workspaces[1])); - await user.click(screen.getByRole("button", { name: /actions/i })); + await user.click(screen.getByRole("button", { name: /bulk actions/i })); const startButton = await screen.findByRole("menuitem", { name: /start/i }); await user.click(startButton); diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index a62c99d591d7c..569e3df0d347c 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -143,7 +143,7 @@ export const WorkspacesPageView: FC = ({ css={{ borderRadius: 9999, marginLeft: "auto" }} endIcon={} > - Actions + Bulk actions From 139548c4731ab2073cebae09914a96094f8c484f Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 13 May 2025 13:19:45 +0000 Subject: [PATCH 12/14] Fix stories --- .../WorkspaceActions/WorkspaceActions.stories.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx index 5fa238eae7ef0..9439b0ca22f3d 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx @@ -8,16 +8,30 @@ import { withDesktopViewport, } from "testHelpers/storybook"; import { WorkspaceActions } from "./WorkspaceActions"; +import { deploymentConfigQueryKey } from "api/queries/deployment"; const meta: Meta = { title: "pages/WorkspacePage/WorkspaceActions", component: WorkspaceActions, args: { isUpdating: false, + permissions: { + deleteFailedWorkspace: true, + deploymentConfig: true, + readWorkspace: true, + updateWorkspace: true, + updateWorkspaceVersion: true, + }, }, decorators: [withDashboardProvider, withDesktopViewport, withAuthProvider], parameters: { user: Mocks.MockUserOwner, + queries: [ + { + key: deploymentConfigQueryKey, + data: Mocks.MockDeploymentConfig, + }, + ], }, }; From 84b65a83d9f47dc30366fe2c36a8891d45056971 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 13 May 2025 20:49:38 +0000 Subject: [PATCH 13/14] Apply PR reviews --- .../ChangeWorkspaceVersionDialog.tsx | 19 ++++------- .../WorkspaceMoreActions.tsx | 24 ++++++++------ .../WorkspaceActions.stories.tsx | 2 +- .../WorkspaceActions/WorkspaceActions.tsx | 2 +- .../WorkspaceParametersPage.tsx | 32 ++++++++++--------- 5 files changed, 40 insertions(+), 39 deletions(-) diff --git a/site/src/modules/workspaces/WorkspaceMoreActions/ChangeWorkspaceVersionDialog.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/ChangeWorkspaceVersionDialog.tsx index 58e3e9d67a68e..4cd59e7b9167b 100644 --- a/site/src/modules/workspaces/WorkspaceMoreActions/ChangeWorkspaceVersionDialog.tsx +++ b/site/src/modules/workspaces/WorkspaceMoreActions/ChangeWorkspaceVersionDialog.tsx @@ -16,7 +16,7 @@ import { Pill } from "components/Pill/Pill"; import { Stack } from "components/Stack/Stack"; import { InfoIcon } from "lucide-react"; import { TemplateUpdateMessage } from "modules/templates/TemplateUpdateMessage"; -import { type FC, useEffect, useState } from "react"; +import { type FC, useState } from "react"; import { useQuery } from "react-query"; import { createDayString } from "utils/createDayString"; @@ -31,28 +31,23 @@ export const ChangeWorkspaceVersionDialog: FC< > = ({ workspace, onClose, onConfirm, ...dialogProps }) => { const { data: versions } = useQuery({ ...templateVersions(workspace.template_id), - select: (data) => data.reverse(), + select: (data) => [...data].reverse(), }); const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false); const activeVersion = versions?.find( (v) => workspace.template_active_version_id === v.id, ); - const [selectedVersion, setSelectedVersion] = useState(); + const [newVersion, setNewVersion] = useState(); const validVersions = versions?.filter((v) => v.job.status === "succeeded"); - - useEffect(() => { - if (activeVersion) { - setSelectedVersion(activeVersion); - } - }, [activeVersion]); + const selectedVersion = newVersion || activeVersion; return ( { - if (selectedVersion) { - onConfirm(selectedVersion); + if (newVersion) { + onConfirm(newVersion); } }} hideCancel={false} @@ -73,7 +68,7 @@ export const ChangeWorkspaceVersionDialog: FC< id="template-version-autocomplete" open={isAutocompleteOpen} onChange={(_, newTemplateVersion) => { - setSelectedVersion(newTemplateVersion); + setNewVersion(newTemplateVersion); }} onOpen={() => { setIsAutocompleteOpen(true); diff --git a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx index 3bc6a471efb91..22e9638ee7caa 100644 --- a/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx +++ b/site/src/modules/workspaces/WorkspaceMoreActions/WorkspaceMoreActions.tsx @@ -21,7 +21,7 @@ import { SettingsIcon, TrashIcon, } from "lucide-react"; -import { type FC, useState } from "react"; +import { type FC, useEffect, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "react-query"; import { Link as RouterLink } from "react-router-dom"; import { ChangeWorkspaceVersionDialog } from "./ChangeWorkspaceVersionDialog"; @@ -32,7 +32,7 @@ import { useWorkspaceDuplication } from "./useWorkspaceDuplication"; type WorkspaceMoreActionsProps = { workspace: Workspace; - disabled?: boolean; + disabled: boolean; }; export const WorkspaceMoreActions: FC = ({ @@ -63,9 +63,17 @@ export const WorkspaceMoreActions: FC = ({ const { duplicateWorkspace, isDuplicationReady } = useWorkspaceDuplication(workspace); + // Since the workspace state is not updated immediately after the mutation, we + // need to be sure the menu is closed when the action gets disabled. + // Reference: https://github.com/coder/coder/pull/17775#discussion_r2087273706 + const [open, setOpen] = useState(false); + useEffect(() => { + setOpen((open) => (disabled ? false : open)); + }); + return ( <> - +