Skip to content

Commit 48de469

Browse files
authored
ENG-1018 Relation migrations, with UX support (#536)
* ENG-1018 : broke up the admin panel in independent units; created the migration admin tab; added the migration information on the source; ensured no duplicates in migration.
1 parent 031b137 commit 48de469

5 files changed

Lines changed: 408 additions & 216 deletions

File tree

apps/roam/src/components/settings/AdminPanel.tsx

Lines changed: 158 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import {
2525
type NodeSignature,
2626
type PConceptFull,
2727
} from "@repo/database/lib/queries";
28+
import migrateRelations from "~/utils/migrateRelations";
29+
import { countReifiedRelations } from "~/utils/createReifiedBlock";
2830
import { DGSupabaseClient } from "@repo/database/lib/client";
2931

3032
const NodeRow = ({ node }: { node: PConceptFull }) => {
@@ -103,7 +105,7 @@ const NodeTable = ({ nodes }: { nodes: PConceptFull[] }) => {
103105
);
104106
};
105107

106-
const AdminPanel = (): React.ReactElement => {
108+
const NodeListTab = (): React.ReactElement => {
107109
const [context, setContext] = useState<SupabaseContext | null>(null);
108110
const [supabase, setSupabase] = useState<DGSupabaseClient | null>(null);
109111
const [schemas, setSchemas] = useState<NodeSignature[]>([]);
@@ -113,11 +115,6 @@ const AdminPanel = (): React.ReactElement => {
113115
const [loading, setLoading] = useState(true);
114116
const [loadingNodes, setLoadingNodes] = useState(true);
115117
const [error, setError] = useState<string | null>(null);
116-
const [selectedTabId, setSelectedTabId] = useState<TabId>("admin");
117-
const [useReifiedRelations, setUseReifiedRelations] = useState<boolean>(
118-
getSetting("use-reified-relations"),
119-
);
120-
121118
useEffect(() => {
122119
let ignore = false;
123120
void (async () => {
@@ -211,6 +208,148 @@ const AdminPanel = (): React.ReactElement => {
211208
return <p className="text-red-700">{error}</p>;
212209
}
213210

211+
return (
212+
<>
213+
<p>
214+
Context:{" "}
215+
<code>{JSON.stringify({ ...context, spacePassword: "****" })}</code>
216+
</p>
217+
{schemas.length > 0 ? (
218+
<>
219+
<Label>
220+
Display:
221+
<div className="mx-2 inline-block">
222+
<Select
223+
items={schemas}
224+
onItemSelect={(choice) => {
225+
setShowingSchema(choice);
226+
}}
227+
itemRenderer={(node, { handleClick, modifiers }) => (
228+
<MenuItem
229+
active={modifiers.active}
230+
key={node.sourceLocalId}
231+
label={node.name}
232+
onClick={handleClick}
233+
text={node.name}
234+
/>
235+
)}
236+
>
237+
<Button text={showingSchema.name} />
238+
</Select>
239+
</div>
240+
</Label>
241+
<div>{loadingNodes ? <Spinner /> : <NodeTable nodes={nodes} />}</div>
242+
</>
243+
) : (
244+
<p>No node schemas found</p>
245+
)}
246+
</>
247+
);
248+
};
249+
250+
const MigrationTab = (): React.ReactElement => {
251+
let initial = true;
252+
const enabled = getSetting("use-reified-relations");
253+
const [useMigrationResults, setMigrationResults] = useState<string>("");
254+
const [useOngoing, setOngoing] = useState<boolean>(false);
255+
const [useDryRun, setDryRun] = useState<boolean>(false);
256+
const doMigrateRelations = async () => {
257+
setOngoing(true);
258+
try {
259+
const before = await countReifiedRelations();
260+
const numProcessed = await migrateRelations(useDryRun);
261+
const after = await countReifiedRelations();
262+
if (after - before < numProcessed)
263+
setMigrationResults(
264+
`${after - before} new relations created out of ${numProcessed} distinct relations processed`,
265+
);
266+
else setMigrationResults(`${numProcessed} new relations created`);
267+
} catch (e) {
268+
console.error("Relation migration failed", e);
269+
setMigrationResults(
270+
`Migration failed: ${(e as Error).message ?? "see console for details"}`,
271+
);
272+
} finally {
273+
setOngoing(false);
274+
}
275+
};
276+
useEffect(() => {
277+
void (async () => {
278+
if (initial) {
279+
const numRelations = await countReifiedRelations();
280+
setMigrationResults(
281+
numRelations > 0
282+
? `${numRelations} already migrated`
283+
: "No migrated relations",
284+
);
285+
// eslint-disable-next-line react-hooks/exhaustive-deps
286+
initial = false;
287+
}
288+
})();
289+
return () => {
290+
initial;
291+
};
292+
}, []);
293+
294+
return (
295+
<>
296+
<p>
297+
<Button
298+
className="p-4"
299+
onClick={() => {
300+
void doMigrateRelations();
301+
}}
302+
disabled={!enabled || useOngoing}
303+
text="Migrate all relations"
304+
></Button>
305+
<Checkbox
306+
className="left-6 inline-block"
307+
defaultChecked={useDryRun}
308+
onChange={(e) => {
309+
const target = e.target as HTMLInputElement;
310+
setDryRun(target.checked);
311+
}}
312+
labelElement={<>Dry run</>}
313+
/>
314+
</p>
315+
{useOngoing ? (
316+
<Spinner />
317+
) : (
318+
<p id="migrationResultsLabel">{useMigrationResults}</p>
319+
)}
320+
</>
321+
);
322+
};
323+
324+
const FeatureFlagsTab = (): React.ReactElement => {
325+
const [useReifiedRelations, setUseReifiedRelations] = useState<boolean>(
326+
getSetting("use-reified-relations"),
327+
);
328+
return (
329+
<Checkbox
330+
defaultChecked={useReifiedRelations}
331+
onChange={(e) => {
332+
const target = e.target as HTMLInputElement;
333+
setUseReifiedRelations(target.checked);
334+
setSetting("use-reified-relations", target.checked);
335+
}}
336+
labelElement={
337+
<>
338+
Reified Relation Triples
339+
<Description
340+
description={
341+
"When ON, relations are read/written as reifiedRelationUid in [[roam/js/discourse-graph/relations]]."
342+
}
343+
/>
344+
</>
345+
}
346+
/>
347+
);
348+
};
349+
350+
const AdminPanel = (): React.ReactElement => {
351+
const [selectedTabId, setSelectedTabId] = useState<TabId>("admin");
352+
214353
return (
215354
<Tabs
216355
onChange={(id) => setSelectedTabId(id)}
@@ -222,70 +361,26 @@ const AdminPanel = (): React.ReactElement => {
222361
title="Admin"
223362
panel={
224363
<div className="flex flex-col gap-4 p-1">
225-
<Checkbox
226-
defaultChecked={useReifiedRelations}
227-
onChange={(e) => {
228-
const target = e.target as HTMLInputElement;
229-
setUseReifiedRelations(target.checked);
230-
setSetting("use-reified-relations", target.checked);
231-
}}
232-
labelElement={
233-
<>
234-
Reified Relation Triples
235-
<Description
236-
description={
237-
"When ON, relations are read/written as reifiedRelationUid in [[roam/js/discourse-graph/relations]]."
238-
}
239-
/>
240-
</>
241-
}
242-
/>
364+
<FeatureFlagsTab />
365+
</div>
366+
}
367+
/>
368+
<Tab
369+
id="migration"
370+
title="Migration"
371+
panel={
372+
<div className="flex flex-col gap-4 p-1">
373+
<MigrationTab />
243374
</div>
244375
}
245376
/>
246377
<Tab
247378
id="node-list"
248379
title="Node list"
249380
panel={
250-
<>
251-
<p>
252-
Context:{" "}
253-
<code>
254-
{JSON.stringify({ ...context, spacePassword: "****" })}
255-
</code>
256-
</p>
257-
{schemas.length > 0 ? (
258-
<>
259-
<Label>
260-
Display:
261-
<div className="mx-2 inline-block">
262-
<Select
263-
items={schemas}
264-
onItemSelect={(choice) => {
265-
setShowingSchema(choice);
266-
}}
267-
itemRenderer={(node, { handleClick, modifiers }) => (
268-
<MenuItem
269-
active={modifiers.active}
270-
key={node.sourceLocalId}
271-
label={node.name}
272-
onClick={handleClick}
273-
text={node.name}
274-
/>
275-
)}
276-
>
277-
<Button text={showingSchema.name} />
278-
</Select>
279-
</div>
280-
</Label>
281-
<div>
282-
{loadingNodes ? <Spinner /> : <NodeTable nodes={nodes} />}
283-
</div>
284-
</>
285-
) : (
286-
<p>No node schemas found</p>
287-
)}
288-
</>
381+
<div className="flex flex-col gap-4 p-1">
382+
<NodeListTab />
383+
</div>
289384
}
290385
/>
291386
</Tabs>

apps/roam/src/utils/createReifiedBlock.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ import createPage from "roamjs-components/writes/createPage";
33
import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle";
44
import { getSetting } from "~/utils/extensionSettings";
55

6+
export const DISCOURSE_GRAPH_PROP_NAME = "discourse-graph";
7+
68
const strictQueryForReifiedBlocks = async (
79
parameterUids: Record<string, string>,
810
): Promise<string | null> => {
911
const paramsAsSeq = Object.entries(parameterUids);
1012
const query = `[:find ?u ?d
1113
:in $ ${paramsAsSeq.map(([k]) => "?" + k).join(" ")}
12-
:where [?s :block/uid ?u] [?s :block/props ?p] [(get ?p :discourse-graph) ?d]
14+
:where [?s :block/uid ?u] [?s :block/props ?p] [(get ?p :${DISCOURSE_GRAPH_PROP_NAME}) ?d]
1315
${paramsAsSeq.map(([k]) => `[(get ?d :${k}) ?_${k}] [(= ?${k} ?_${k})]`).join(" ")} ]`;
1416
// Note: the extra _k binding variable is only needed for the backend query somehow
1517
// In a local query, we can directly map to `[(get ?d :${k}) ?${k}]`
@@ -58,7 +60,7 @@ const createReifiedBlock = async ({
5860
uid: newUid,
5961
props: {
6062
// eslint-disable-next-line @typescript-eslint/naming-convention
61-
"discourse-graph": data,
63+
[DISCOURSE_GRAPH_PROP_NAME]: data,
6264
},
6365
},
6466
parentUid: destinationBlockUid,
@@ -80,6 +82,15 @@ const getRelationPageUid = async (): Promise<string> => {
8082
return relationPageUid;
8183
};
8284

85+
export const countReifiedRelations = async (): Promise<number> => {
86+
const pageUid = await getRelationPageUid();
87+
if (pageUid === undefined) return 0;
88+
const r = await window.roamAlphaAPI.data.async.q(
89+
`[:find (count ?c) :where [?p :block/children ?c] [?p :block/uid "${pageUid}"]]`,
90+
);
91+
return (r[0] || [0])[0] as number;
92+
};
93+
8394
export const createReifiedRelation = async ({
8495
sourceUid,
8596
relationBlockUid,

0 commit comments

Comments
 (0)