Skip to content

Commit e82c63a

Browse files
committed
wip: commit current progress
1 parent d07ba17 commit e82c63a

File tree

2 files changed

+140
-0
lines changed

2 files changed

+140
-0
lines changed

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,5 +60,8 @@
6060
"typos.config": ".github/workflows/typos.toml",
6161
"[markdown]": {
6262
"editor.defaultFormatter": "DavidAnson.vscode-markdownlint"
63+
},
64+
"[typescriptreact]": {
65+
"editor.defaultFormatter": "esbenp.prettier-vscode"
6366
}
6467
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { TemplateVersion, type Workspace } from "api/typesGenerated";
2+
import { type FC, useMemo, useState } from "react";
3+
import { Dialog, DialogContent } from "components/Dialog/Dialog";
4+
import { Button } from "components/Button/Button";
5+
import { useQueries } from "react-query";
6+
import { templateVersion } from "api/queries/templates";
7+
8+
/**
9+
* @todo Need to decide if we should include the template display name here, or
10+
* if we'll be able to get that data as part of other fetches. It's also
11+
* possible that the new UX might not require it at all?
12+
*/
13+
type TemplateVersionGroup = Readonly<{
14+
templateVersionId: string;
15+
affectedWorkspaces: readonly Workspace[];
16+
}>;
17+
18+
function groupWorkspacesByTemplateVersionId(
19+
workspaces: readonly Workspace[],
20+
): readonly TemplateVersionGroup[] {
21+
const grouped = new Map<string, TemplateVersionGroup>();
22+
23+
for (const ws of workspaces) {
24+
const templateVersionId = ws.latest_build.template_version_id;
25+
const value = grouped.get(templateVersionId);
26+
if (value !== undefined) {
27+
// Need to do type assertion to make value mutable as an
28+
// implementation detail. Doing things the "proper" way adds a bunch
29+
// of needless boilerplate for a single-line computation
30+
const target = value.affectedWorkspaces as Workspace[];
31+
target.push(ws);
32+
continue;
33+
}
34+
35+
grouped.set(templateVersionId, {
36+
templateVersionId,
37+
affectedWorkspaces: [ws],
38+
});
39+
}
40+
41+
return [...grouped.values()];
42+
}
43+
44+
type WorkspaceDeltaEntry = Readonly<{}>;
45+
type WorkspaceDeltas = Map<string, WorkspaceDeltaEntry | null>;
46+
47+
function separateWorkspacesByDormancy(
48+
workspaces: readonly Workspace[],
49+
): readonly [dormant: readonly Workspace[], active: readonly Workspace[]] {
50+
const dormant: Workspace[] = [];
51+
const active: Workspace[] = [];
52+
53+
for (const ws of workspaces) {
54+
// If a workspace doesn't have any pending updates whatsoever, we can
55+
// safely skip processing it
56+
if (!ws.outdated) {
57+
continue;
58+
}
59+
if (ws.dormant_at) {
60+
dormant.push(ws);
61+
} else {
62+
active.push(ws);
63+
}
64+
}
65+
66+
return [dormant, active];
67+
}
68+
69+
type BatchUpdateModalFormProps = Readonly<{
70+
workspacesToUpdate: readonly Workspace[];
71+
onClose: () => void;
72+
onSubmit: () => void;
73+
}>;
74+
75+
export const BatchUpdateModalForm: FC<BatchUpdateModalFormProps> = ({
76+
workspacesToUpdate,
77+
onClose,
78+
onSubmit,
79+
}) => {
80+
// We need to take a local snapshot of the workspaces that existed on mount
81+
// because workspaces are such a mutable resource, and there's a chance that
82+
// 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
85+
const [cachedWorkspaces, setCachedWorkspaces] = useState(workspacesToUpdate);
86+
// Dormant workspaces can't be activated without activating them first. For
87+
// now, we'll only show the user that some workspaces can't be updated, and
88+
// then skip over them for all other update logic
89+
const [dormant, active] = separateWorkspacesByDormancy(cachedWorkspaces);
90+
91+
// The workspaces don't have all necessary data by themselves, so we need to
92+
// fetch the unique template versions, and massage the results
93+
const groups = groupWorkspacesByTemplateVersionId(active);
94+
const templateVersionQueries = useQueries({
95+
queries: groups.map((g) => templateVersion(g.templateVersionId)),
96+
});
97+
// 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
99+
const error = templateVersionQueries.find((q) => q.isError)?.error;
100+
const merged = templateVersionQueries.every((q) => q.isSuccess)
101+
? templateVersionQueries.map((q) => q.data)
102+
: undefined;
103+
104+
// Also need to tease apart workspaces that are actively running, because
105+
// 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;
108+
109+
const deltas = useMemo<WorkspaceDeltas>(() => new Map(), []);
110+
111+
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+
}}
121+
>
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>
132+
</div>
133+
</form>
134+
</DialogContent>
135+
</Dialog>
136+
);
137+
};

0 commit comments

Comments
 (0)