@@ -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" ;
2830import { DGSupabaseClient } from "@repo/database/lib/client" ;
2931
3032const 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 >
0 commit comments