Skip to content

Commit d9b00e4

Browse files
feat: add inline actions into workspaces table (#17636)
Related to #17311 This PR adds inline actions in the workspaces page. It is a bit different of the [original design](https://www.figma.com/design/OR75XeUI0Z3ksqt1mHsNQw/Workspace-views?node-id=656-3979&m=dev) because I'm splitting the work into three phases that I will explain in more details in the demo. https://github.com/user-attachments/assets/6383375e-ed10-45d1-b5d5-b4421e86d158
1 parent 5f516ed commit d9b00e4

19 files changed

+495
-127
lines changed

site/src/api/queries/workspaces.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,9 @@ function workspacesKey(config: WorkspacesRequest = {}) {
139139
}
140140

141141
export function workspaces(config: WorkspacesRequest = {}) {
142-
// Duplicates some of the work from workspacesKey, but that felt better than
143-
// letting invisible properties sneak into the query logic
144-
const { q, limit } = config;
145-
146142
return {
147143
queryKey: workspacesKey(config),
148-
queryFn: () => API.getWorkspaces({ q, limit }),
144+
queryFn: () => API.getWorkspaces(config),
149145
} as const satisfies QueryOptions<WorkspacesResponse>;
150146
}
151147

@@ -281,7 +277,10 @@ const updateWorkspaceBuild = async (
281277
build.workspace_owner_name,
282278
build.workspace_name,
283279
);
284-
const previousData = queryClient.getQueryData(workspaceKey) as Workspace;
280+
const previousData = queryClient.getQueryData<Workspace>(workspaceKey);
281+
if (!previousData) {
282+
return;
283+
}
285284

286285
// Check if the build returned is newer than the previous build that could be
287286
// updated from web socket

site/src/components/Dialogs/Dialog.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,14 @@ export const DialogActionButtons: FC<DialogActionButtonsProps> = ({
3535
return (
3636
<>
3737
{onCancel && (
38-
<Button disabled={confirmLoading} onClick={onCancel} variant="outline">
38+
<Button
39+
disabled={confirmLoading}
40+
onClick={(e) => {
41+
e.stopPropagation();
42+
onCancel();
43+
}}
44+
variant="outline"
45+
>
3946
{cancelText}
4047
</Button>
4148
)}

site/src/hooks/usePagination.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export const usePagination = ({
99
const [searchParams, setSearchParams] = searchParamsResult;
1010
const page = searchParams.get("page") ? Number(searchParams.get("page")) : 1;
1111
const limit = DEFAULT_RECORDS_PER_PAGE;
12-
const offset = page <= 0 ? 0 : (page - 1) * limit;
12+
const offset = calcOffset(page, limit);
1313

1414
const goToPage = (page: number) => {
1515
searchParams.set("page", page.toString());
@@ -23,3 +23,7 @@ export const usePagination = ({
2323
offset,
2424
};
2525
};
26+
27+
export const calcOffset = (page: number, limit: number) => {
28+
return page <= 0 ? 0 : (page - 1) * limit;
29+
};
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { MissingBuildParameters } from "api/api";
2+
import { updateWorkspace } from "api/queries/workspaces";
3+
import type {
4+
TemplateVersion,
5+
Workspace,
6+
WorkspaceBuild,
7+
WorkspaceBuildParameter,
8+
} from "api/typesGenerated";
9+
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
10+
import { MemoizedInlineMarkdown } from "components/Markdown/Markdown";
11+
import { UpdateBuildParametersDialog } from "pages/WorkspacePage/UpdateBuildParametersDialog";
12+
import { type FC, useState } from "react";
13+
import { useMutation, useQueryClient } from "react-query";
14+
15+
type UseWorkspaceUpdateOptions = {
16+
workspace: Workspace;
17+
latestVersion: TemplateVersion | undefined;
18+
onSuccess?: (build: WorkspaceBuild) => void;
19+
onError?: (error: unknown) => void;
20+
};
21+
22+
type UseWorkspaceUpdateResult = {
23+
update: () => void;
24+
isUpdating: boolean;
25+
dialogs: {
26+
updateConfirmation: UpdateConfirmationDialogProps;
27+
missingBuildParameters: MissingBuildParametersDialogProps;
28+
};
29+
};
30+
31+
export const useWorkspaceUpdate = ({
32+
workspace,
33+
latestVersion,
34+
onSuccess,
35+
onError,
36+
}: UseWorkspaceUpdateOptions): UseWorkspaceUpdateResult => {
37+
const queryClient = useQueryClient();
38+
const [isConfirmingUpdate, setIsConfirmingUpdate] = useState(false);
39+
40+
const updateWorkspaceOptions = updateWorkspace(workspace, queryClient);
41+
const updateWorkspaceMutation = useMutation({
42+
...updateWorkspaceOptions,
43+
onSuccess: (build: WorkspaceBuild) => {
44+
updateWorkspaceOptions.onSuccess(build);
45+
onSuccess?.(build);
46+
},
47+
onError,
48+
});
49+
50+
const update = () => {
51+
setIsConfirmingUpdate(true);
52+
};
53+
54+
const confirmUpdate = (buildParameters: WorkspaceBuildParameter[] = []) => {
55+
updateWorkspaceMutation.mutate(buildParameters);
56+
setIsConfirmingUpdate(false);
57+
};
58+
59+
return {
60+
update,
61+
isUpdating: updateWorkspaceMutation.isLoading,
62+
dialogs: {
63+
updateConfirmation: {
64+
open: isConfirmingUpdate,
65+
onClose: () => setIsConfirmingUpdate(false),
66+
onConfirm: () => confirmUpdate(),
67+
latestVersion,
68+
},
69+
missingBuildParameters: {
70+
error: updateWorkspaceMutation.error,
71+
onClose: () => {
72+
updateWorkspaceMutation.reset();
73+
},
74+
onUpdate: (buildParameters: WorkspaceBuildParameter[]) => {
75+
if (updateWorkspaceMutation.error instanceof MissingBuildParameters) {
76+
confirmUpdate(buildParameters);
77+
}
78+
},
79+
},
80+
},
81+
};
82+
};
83+
84+
type WorkspaceUpdateDialogsProps = {
85+
updateConfirmation: UpdateConfirmationDialogProps;
86+
missingBuildParameters: MissingBuildParametersDialogProps;
87+
};
88+
89+
export const WorkspaceUpdateDialogs: FC<WorkspaceUpdateDialogsProps> = ({
90+
updateConfirmation,
91+
missingBuildParameters,
92+
}) => {
93+
return (
94+
<>
95+
<UpdateConfirmationDialog {...updateConfirmation} />
96+
<MissingBuildParametersDialog {...missingBuildParameters} />
97+
</>
98+
);
99+
};
100+
101+
type UpdateConfirmationDialogProps = {
102+
open: boolean;
103+
onClose: () => void;
104+
onConfirm: () => void;
105+
latestVersion?: TemplateVersion;
106+
};
107+
108+
const UpdateConfirmationDialog: FC<UpdateConfirmationDialogProps> = ({
109+
latestVersion,
110+
...dialogProps
111+
}) => {
112+
return (
113+
<ConfirmDialog
114+
{...dialogProps}
115+
hideCancel={false}
116+
title="Update workspace?"
117+
confirmText="Update"
118+
description={
119+
<div className="flex flex-col gap-2">
120+
<p>
121+
Updating your workspace will start the workspace on the latest
122+
template version. This can{" "}
123+
<strong>delete non-persistent data</strong>.
124+
</p>
125+
{latestVersion?.message && (
126+
<MemoizedInlineMarkdown allowedElements={["ol", "ul", "li"]}>
127+
{latestVersion.message}
128+
</MemoizedInlineMarkdown>
129+
)}
130+
</div>
131+
}
132+
/>
133+
);
134+
};
135+
136+
type MissingBuildParametersDialogProps = {
137+
error: unknown;
138+
onClose: () => void;
139+
onUpdate: (buildParameters: WorkspaceBuildParameter[]) => void;
140+
};
141+
142+
const MissingBuildParametersDialog: FC<MissingBuildParametersDialogProps> = ({
143+
error,
144+
...dialogProps
145+
}) => {
146+
return (
147+
<UpdateBuildParametersDialog
148+
missedParameters={
149+
error instanceof MissingBuildParameters ? error.parameters : []
150+
}
151+
open={error instanceof MissingBuildParameters}
152+
{...dialogProps}
153+
/>
154+
);
155+
};

site/src/pages/WorkspacePage/WorkspaceActions/constants.ts renamed to site/src/modules/workspaces/actions.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ const actionTypes = [
3434

3535
export type ActionType = (typeof actionTypes)[number];
3636

37+
type ActionPermissions = {
38+
canDebug: boolean;
39+
isOwner: boolean;
40+
};
41+
3742
type WorkspaceAbilities = {
3843
actions: readonly ActionType[];
3944
canCancel: boolean;
@@ -42,8 +47,11 @@ type WorkspaceAbilities = {
4247

4348
export const abilitiesByWorkspaceStatus = (
4449
workspace: Workspace,
45-
canDebug: boolean,
50+
permissions: ActionPermissions,
4651
): WorkspaceAbilities => {
52+
const hasPermissionToCancel =
53+
workspace.template_allow_user_cancel_workspace_jobs || permissions.isOwner;
54+
4755
if (workspace.dormant_at) {
4856
return {
4957
actions: ["activate"],
@@ -58,7 +66,7 @@ export const abilitiesByWorkspaceStatus = (
5866
case "starting": {
5967
return {
6068
actions: ["starting"],
61-
canCancel: true,
69+
canCancel: hasPermissionToCancel,
6270
canAcceptJobs: false,
6371
};
6472
}
@@ -83,7 +91,7 @@ export const abilitiesByWorkspaceStatus = (
8391
case "stopping": {
8492
return {
8593
actions: ["stopping"],
86-
canCancel: true,
94+
canCancel: hasPermissionToCancel,
8795
canAcceptJobs: false,
8896
};
8997
}
@@ -115,7 +123,7 @@ export const abilitiesByWorkspaceStatus = (
115123
case "failed": {
116124
const actions: ActionType[] = ["retry"];
117125

118-
if (canDebug) {
126+
if (permissions.canDebug) {
119127
actions.push("debug");
120128
}
121129

site/src/pages/TemplateSettingsPage/TemplateSchedulePage/useWorkspacesToBeDeleted.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Template, Workspace } from "api/typesGenerated";
22
import { compareAsc } from "date-fns";
3+
import { calcOffset } from "hooks/usePagination";
34
import { useWorkspacesData } from "pages/WorkspacesPage/data";
45
import type { TemplateScheduleFormValues } from "./formHelpers";
56

@@ -9,9 +10,9 @@ export const useWorkspacesToGoDormant = (
910
fromDate: Date,
1011
) => {
1112
const { data } = useWorkspacesData({
12-
page: 0,
13+
offset: calcOffset(0, 0),
1314
limit: 0,
14-
query: `template:${template.name}`,
15+
q: `template:${template.name}`,
1516
});
1617

1718
return data?.workspaces?.filter((workspace: Workspace) => {
@@ -40,9 +41,9 @@ export const useWorkspacesToBeDeleted = (
4041
fromDate: Date,
4142
) => {
4243
const { data } = useWorkspacesData({
43-
page: 0,
44+
offset: calcOffset(0, 0),
4445
limit: 0,
45-
query: `template:${template.name} dormant:true`,
46+
q: `template:${template.name} dormant:true`,
4647
});
4748
return data?.workspaces?.filter((workspace: Workspace) => {
4849
if (!workspace.dormant_at || !formValues.time_til_dormant_autodelete_ms) {

site/src/pages/WorkspacePage/Workspace.stories.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from "@storybook/react";
33
import type { ProvisionerJobLog } from "api/typesGenerated";
44
import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext";
55
import * as Mocks from "testHelpers/entities";
6-
import { withDashboardProvider } from "testHelpers/storybook";
6+
import { withAuthProvider, withDashboardProvider } from "testHelpers/storybook";
77
import { Workspace } from "./Workspace";
88
import type { WorkspacePermissions } from "./permissions";
99

@@ -40,8 +40,10 @@ const meta: Meta<typeof Workspace> = {
4040
data: Mocks.MockListeningPortsResponse,
4141
},
4242
],
43+
user: Mocks.MockUser,
4344
},
4445
decorators: [
46+
withAuthProvider,
4547
withDashboardProvider,
4648
(Story) => (
4749
<ProxyContext.Provider

site/src/pages/WorkspacePage/Workspace.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ export interface WorkspaceProps {
5353
buildLogs?: TypesGen.ProvisionerJobLog[];
5454
latestVersion?: TypesGen.TemplateVersion;
5555
permissions: WorkspacePermissions;
56-
isOwner: boolean;
5756
timings?: TypesGen.WorkspaceBuildTimings;
5857
}
5958

@@ -86,7 +85,6 @@ export const Workspace: FC<WorkspaceProps> = ({
8685
buildLogs,
8786
latestVersion,
8887
permissions,
89-
isOwner,
9088
timings,
9189
}) => {
9290
const navigate = useNavigate();
@@ -161,7 +159,6 @@ export const Workspace: FC<WorkspaceProps> = ({
161159
isUpdating={isUpdating}
162160
isRestarting={isRestarting}
163161
canUpdateWorkspace={permissions.updateWorkspace}
164-
isOwner={isOwner}
165162
template={template}
166163
permissions={permissions}
167164
latestVersion={latestVersion}

site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.stories.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { expect, userEvent, within } from "@storybook/test";
33
import { agentLogsKey, buildLogsKey } from "api/queries/workspaces";
44
import * as Mocks from "testHelpers/entities";
55
import {
6+
withAuthProvider,
67
withDashboardProvider,
78
withDesktopViewport,
89
} from "testHelpers/storybook";
@@ -14,7 +15,10 @@ const meta: Meta<typeof WorkspaceActions> = {
1415
args: {
1516
isUpdating: false,
1617
},
17-
decorators: [withDashboardProvider, withDesktopViewport],
18+
decorators: [withDashboardProvider, withDesktopViewport, withAuthProvider],
19+
parameters: {
20+
user: Mocks.MockUser,
21+
},
1822
};
1923

2024
export default meta;
@@ -163,14 +167,15 @@ export const CancelShownForOwner: Story = {
163167
...Mocks.MockStartingWorkspace,
164168
template_allow_user_cancel_workspace_jobs: false,
165169
},
166-
isOwner: true,
167170
},
168171
};
169172

170173
export const CancelShownForUser: Story = {
171174
args: {
172175
workspace: Mocks.MockStartingWorkspace,
173-
isOwner: false,
176+
},
177+
parameters: {
178+
user: Mocks.MockUser2,
174179
},
175180
};
176181

@@ -180,7 +185,9 @@ export const CancelHiddenForUser: Story = {
180185
...Mocks.MockStartingWorkspace,
181186
template_allow_user_cancel_workspace_jobs: false,
182187
},
183-
isOwner: false,
188+
},
189+
parameters: {
190+
user: Mocks.MockUser2,
184191
},
185192
};
186193

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

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

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


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy