Skip to content

Commit 696ec33

Browse files
committed
wip: commit more progress
1 parent e82c63a commit 696ec33

File tree

2 files changed

+198
-68
lines changed

2 files changed

+198
-68
lines changed

site/src/pages/WorkspacesPage/BatchUpdateModalForm.tsx

Lines changed: 171 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
import { TemplateVersion, type Workspace } from "api/typesGenerated";
2-
import { type FC, useMemo, useState } from "react";
1+
import type { Workspace } from "api/typesGenerated";
2+
import { type FC, ReactElement, ReactNode, useState } from "react";
33
import { Dialog, DialogContent } from "components/Dialog/Dialog";
44
import { Button } from "components/Button/Button";
55
import { useQueries } from "react-query";
66
import { templateVersion } from "api/queries/templates";
7+
import { Loader } from "components/Loader/Loader";
8+
import { ErrorAlert } from "components/Alert/ErrorAlert";
9+
import { Avatar } from "components/Avatar/Avatar";
710

811
/**
912
* @todo Need to decide if we should include the template display name here, or
@@ -41,96 +44,218 @@ function groupWorkspacesByTemplateVersionId(
4144
return [...grouped.values()];
4245
}
4346

44-
type WorkspaceDeltaEntry = Readonly<{}>;
45-
type WorkspaceDeltas = Map<string, WorkspaceDeltaEntry | null>;
47+
type Separation = Readonly<{
48+
dormant: readonly Workspace[];
49+
noUpdateNeeded: readonly Workspace[];
50+
readyToUpdate: readonly Workspace[];
51+
}>;
4652

47-
function separateWorkspacesByDormancy(
48-
workspaces: readonly Workspace[],
49-
): readonly [dormant: readonly Workspace[], active: readonly Workspace[]] {
53+
function separateWorkspaces(workspaces: readonly Workspace[]): Separation {
54+
const noUpdateNeeded: Workspace[] = [];
5055
const dormant: Workspace[] = [];
51-
const active: Workspace[] = [];
56+
const readyToUpdate: Workspace[] = [];
5257

5358
for (const ws of workspaces) {
54-
// If a workspace doesn't have any pending updates whatsoever, we can
55-
// safely skip processing it
5659
if (!ws.outdated) {
60+
noUpdateNeeded.push(ws);
5761
continue;
5862
}
59-
if (ws.dormant_at) {
63+
if (ws.dormant_at !== null) {
6064
dormant.push(ws);
61-
} else {
62-
active.push(ws);
65+
continue;
6366
}
67+
readyToUpdate.push(ws);
6468
}
6569

66-
return [dormant, active];
70+
return { dormant, noUpdateNeeded, readyToUpdate };
6771
}
6872

69-
type BatchUpdateModalFormProps = Readonly<{
73+
type WorkspacePanelProps = Readonly<{
74+
workspaceName: string;
75+
workspaceIconUrl: string;
76+
label?: ReactNode;
77+
adornment?: ReactNode;
78+
}>;
79+
80+
const ReviewPanel: FC<WorkspacePanelProps> = ({
81+
workspaceName,
82+
label,
83+
workspaceIconUrl,
84+
}) => {
85+
return (
86+
<div className="rounded-md px-4 py-2 border border-solid border-content-secondary/50 text-sm">
87+
<div className="flex flex-row flex-wrap grow items-center gap-2">
88+
<Avatar size="sm" variant="icon" src={workspaceIconUrl} />
89+
{workspaceName}
90+
</div>
91+
</div>
92+
);
93+
};
94+
95+
type ReviewFormProps = Readonly<{
7096
workspacesToUpdate: readonly Workspace[];
71-
onClose: () => void;
97+
onCancel: () => void;
7298
onSubmit: () => void;
7399
}>;
74100

75-
export const BatchUpdateModalForm: FC<BatchUpdateModalFormProps> = ({
101+
const ReviewForm: FC<ReviewFormProps> = ({
76102
workspacesToUpdate,
77-
onClose,
103+
onCancel,
78104
onSubmit,
79105
}) => {
80106
// We need to take a local snapshot of the workspaces that existed on mount
81107
// because workspaces are such a mutable resource, and there's a chance that
82108
// they can be changed by another user + be subject to a query invalidation
83-
// while the form is open. We need to cross-reference these with the latest
84-
// workspaces from props so that we can display any changes in the UI
109+
// while the form is open
85110
const [cachedWorkspaces, setCachedWorkspaces] = useState(workspacesToUpdate);
86111
// Dormant workspaces can't be activated without activating them first. For
87112
// now, we'll only show the user that some workspaces can't be updated, and
88113
// then skip over them for all other update logic
89-
const [dormant, active] = separateWorkspacesByDormancy(cachedWorkspaces);
114+
const { dormant, noUpdateNeeded, readyToUpdate } =
115+
separateWorkspaces(cachedWorkspaces);
90116

91117
// The workspaces don't have all necessary data by themselves, so we need to
92118
// fetch the unique template versions, and massage the results
93-
const groups = groupWorkspacesByTemplateVersionId(active);
119+
const groups = groupWorkspacesByTemplateVersionId(readyToUpdate);
94120
const templateVersionQueries = useQueries({
95121
queries: groups.map((g) => templateVersion(g.templateVersionId)),
96122
});
97123
// React Query persists previous errors even if a query is no longer in the
98-
// error state, so we need to explicitly check the isError property
124+
// error state, so we need to explicitly check the isError property to see
125+
// if any of the queries actively have an error
99126
const error = templateVersionQueries.find((q) => q.isError)?.error;
100127
const merged = templateVersionQueries.every((q) => q.isSuccess)
101128
? templateVersionQueries.map((q) => q.data)
102129
: undefined;
103130

104131
// Also need to tease apart workspaces that are actively running, because
105132
// there's a whole set of warnings we need to issue about them
106-
const running = active.filter((a) => a.latest_build.status === "running");
107-
const workspacesChangedWhileOpen = workspacesToUpdate !== cachedWorkspaces;
133+
const running = readyToUpdate.filter(
134+
(ws) => ws.latest_build.status === "running",
135+
);
108136

109-
const deltas = useMemo<WorkspaceDeltas>(() => new Map(), []);
137+
const workspacesChangedWhileOpen = workspacesToUpdate !== cachedWorkspaces;
138+
const updateIsReady = error !== undefined && readyToUpdate.length > 0;
110139

111140
return (
112-
<Dialog>
113-
<DialogContent>
114-
<form
115-
className="max-w-lg px-4"
116-
onSubmit={(e) => {
117-
e.preventDefault();
118-
console.log("Blah");
119-
onSubmit();
120-
}}
141+
<form
142+
className="overflow-y-auto max-h-[90vh]"
143+
onSubmit={(e) => {
144+
e.preventDefault();
145+
onSubmit();
146+
}}
147+
>
148+
<div className="flex flex-row justify-between items-center pb-6">
149+
<h3 className="text-2xl font-semibold m-0 leading-tight">
150+
Review update
151+
</h3>
152+
153+
<Button
154+
variant="outline"
155+
disabled={!workspacesChangedWhileOpen}
156+
onClick={() => setCachedWorkspaces(workspacesToUpdate)}
121157
>
122-
<div className="flex flex-row justify-between">
123-
<h2 className="text-xl font-semibold m-0 leading-tight">
124-
Review updates
125-
</h2>
126-
<Button
127-
disabled={workspacesChangedWhileOpen}
128-
onClick={() => setCachedWorkspaces(workspacesToUpdate)}
129-
>
130-
Refresh
131-
</Button>
158+
Refresh list
159+
</Button>
160+
</div>
161+
162+
{error !== undefined && <ErrorAlert error={error} />}
163+
164+
{noUpdateNeeded.length > 0 && (
165+
<section className="border-0 border-t border-solid border-t-content-secondary/25 py-4">
166+
<div className="max-w-prose">
167+
<h4 className="m-0">Updated workspaces</h4>
168+
<p className="m-0 text-sm leading-snug text-content-secondary">
169+
These workspaces are fully up to date and will be skipped during
170+
the update.
171+
</p>
132172
</div>
133-
</form>
173+
174+
<ul className="list-none p-0">
175+
{noUpdateNeeded.map((ws) => (
176+
<li key={ws.id}>
177+
<ReviewPanel
178+
workspaceName={ws.name}
179+
workspaceIconUrl={ws.template_icon}
180+
/>
181+
</li>
182+
))}
183+
</ul>
184+
</section>
185+
)}
186+
187+
{dormant.length > 0 && (
188+
<section className="border-0 border-t border-solid border-t-content-secondary/25 py-4">
189+
<div className="max-w-prose">
190+
<h4 className="m-0">Dormant workspaces</h4>
191+
<p className="m-0 text-sm leading-snug text-content-secondary">
192+
Dormant workspaces cannot be updated without first activating the
193+
workspace. They will be skipped during the batch update.
194+
</p>
195+
</div>
196+
197+
<ul className="list-none p-0">
198+
{dormant.map((ws) => (
199+
<li key={ws.id}>
200+
<ReviewPanel
201+
workspaceName={ws.name}
202+
workspaceIconUrl={ws.template_icon}
203+
/>
204+
</li>
205+
))}
206+
</ul>
207+
</section>
208+
)}
209+
210+
<div className="flex flex-row flex-wrap justify-end gap-4">
211+
<Button variant="outline" onClick={onCancel}>
212+
Cancel
213+
</Button>
214+
<Button variant="default" type="submit" disabled={!updateIsReady}>
215+
Update
216+
</Button>
217+
</div>
218+
</form>
219+
);
220+
};
221+
222+
type BatchUpdateModalFormProps = Readonly<{
223+
workspacesToUpdate: readonly Workspace[];
224+
open: boolean;
225+
loading: boolean;
226+
onClose: () => void;
227+
onSubmit: () => void;
228+
}>;
229+
230+
export const BatchUpdateModalForm: FC<BatchUpdateModalFormProps> = ({
231+
open,
232+
loading,
233+
workspacesToUpdate,
234+
onClose,
235+
onSubmit,
236+
}) => {
237+
return (
238+
<Dialog
239+
open={open}
240+
onOpenChange={() => {
241+
if (open) {
242+
onClose();
243+
}
244+
}}
245+
>
246+
<DialogContent className="max-w-screen-md">
247+
{loading ? (
248+
<Loader />
249+
) : (
250+
<ReviewForm
251+
workspacesToUpdate={workspacesToUpdate}
252+
onCancel={onClose}
253+
onSubmit={() => {
254+
onSubmit();
255+
onClose();
256+
}}
257+
/>
258+
)}
134259
</DialogContent>
135260
</Dialog>
136261
);

site/src/pages/WorkspacesPage/WorkspacesPage.tsx

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ import { useQuery, useQueryClient } from "react-query";
1717
import { useSearchParams } from "react-router-dom";
1818
import { pageTitle } from "utils/page";
1919
import { BatchDeleteConfirmation } from "./BatchDeleteConfirmation";
20-
import { BatchUpdateConfirmation } from "./BatchUpdateConfirmation";
2120
import { WorkspacesPageView } from "./WorkspacesPageView";
2221
import { useBatchActions } from "./batchActions";
2322
import { useStatusFilterMenu, useTemplateFilterMenu } from "./filter/menus";
23+
import { BatchUpdateModalForm } from "./BatchUpdateModalForm";
2424

2525
function useSafeSearchParams(): ReturnType<typeof useSearchParams> {
2626
// Have to wrap setSearchParams because React Router doesn't make sure that
@@ -36,6 +36,8 @@ function useSafeSearchParams(): ReturnType<typeof useSearchParams> {
3636
>;
3737
}
3838

39+
type BatchAction = "delete" | "update";
40+
3941
const WorkspacesPage: FC = () => {
4042
const queryClient = useQueryClient();
4143
// If we use a useSearchParams for each hook, the values will not be in sync.
@@ -89,9 +91,7 @@ const WorkspacesPage: FC = () => {
8991
const [checkedWorkspaces, setCheckedWorkspaces] = useState<
9092
readonly Workspace[]
9193
>([]);
92-
const [confirmingBatchAction, setConfirmingBatchAction] = useState<
93-
"delete" | "update" | null
94-
>(null);
94+
const [activeBatchAction, setActiveBatchAction] = useState<BatchAction>();
9595
const canCheckWorkspaces =
9696
entitlements.features.workspace_batch_actions.enabled;
9797
const batchActions = useBatchActions({
@@ -130,8 +130,8 @@ const WorkspacesPage: FC = () => {
130130
onPageChange={pagination.goToPage}
131131
filterProps={filterProps}
132132
isRunningBatchAction={batchActions.isLoading}
133-
onDeleteAll={() => setConfirmingBatchAction("delete")}
134-
onUpdateAll={() => setConfirmingBatchAction("update")}
133+
onDeleteAll={() => setActiveBatchAction("delete")}
134+
onUpdateAll={() => setActiveBatchAction("update")}
135135
onStartAll={() => batchActions.startAll(checkedWorkspaces)}
136136
onStopAll={() => batchActions.stopAll(checkedWorkspaces)}
137137
onActionSuccess={async () => {
@@ -150,29 +150,34 @@ const WorkspacesPage: FC = () => {
150150
<BatchDeleteConfirmation
151151
isLoading={batchActions.isLoading}
152152
checkedWorkspaces={checkedWorkspaces}
153-
open={confirmingBatchAction === "delete"}
153+
open={activeBatchAction === "delete"}
154154
onConfirm={async () => {
155155
await batchActions.deleteAll(checkedWorkspaces);
156-
setConfirmingBatchAction(null);
156+
setActiveBatchAction(undefined);
157157
}}
158158
onClose={() => {
159-
setConfirmingBatchAction(null);
159+
setActiveBatchAction(undefined);
160160
}}
161161
/>
162162

163-
<BatchUpdateConfirmation
164-
isLoading={batchActions.isLoading}
165-
checkedWorkspaces={checkedWorkspaces}
166-
open={confirmingBatchAction === "update"}
167-
onConfirm={async () => {
168-
await batchActions.updateAll({
169-
workspaces: checkedWorkspaces,
170-
isDynamicParametersEnabled: false,
171-
});
172-
setConfirmingBatchAction(null);
173-
}}
174-
onClose={() => {
175-
setConfirmingBatchAction(null);
163+
<BatchUpdateModalForm
164+
open={activeBatchAction === "update"}
165+
loading={batchActions.isLoading}
166+
workspacesToUpdate={checkedWorkspaces}
167+
onClose={() => setActiveBatchAction(undefined)}
168+
onSubmit={async () => {
169+
console.log("Hooray!");
170+
/**
171+
* @todo Make sure this gets added back in once more of the
172+
* component has been fleshed out
173+
*/
174+
if (false) {
175+
await batchActions.updateAll({
176+
workspaces: checkedWorkspaces,
177+
isDynamicParametersEnabled: false,
178+
});
179+
}
180+
setActiveBatchAction(undefined);
176181
}}
177182
/>
178183
</>

0 commit comments

Comments
 (0)