Skip to content

Instantly share code, notes, and snippets.

@mdroidian
Last active September 15, 2025 06:28
Show Gist options
  • Select an option

  • Save mdroidian/88dc865156bef4359a860e9f962203b1 to your computer and use it in GitHub Desktop.

Select an option

Save mdroidian/88dc865156bef4359a860e9f962203b1 to your computer and use it in GitHub Desktop.
import React, { useEffect, useState, useCallback } from "react";
import type { OnloadArgs } from "roamjs-components/types";
import { HTMLTable, Button, MenuItem, Spinner } from "@blueprintjs/core";
import { Select } from "@blueprintjs/select";
import {
getSupabaseContext,
getLoggedInClient,
type SupabaseContext,
} from "~/utils/supabaseContext";
import type { Tables } from "@repo/database/dbTypes";
import {
getNodes,
getNodeSchemas,
type NodeSignature,
type PConcept,
} from "@repo/database/lib/queries";
import { DGSupabaseClient } from "@repo/database/lib/client";
type AdminPanelProps = {
onloadArgs: OnloadArgs;
};
const AdminPanel = ({ onloadArgs }: AdminPanelProps) => {
const [context, setContext] = useState<SupabaseContext | null>(null);
const [supabase, setSupabase] = useState<DGSupabaseClient | null>(null);
const [schemas, setSchemas] = useState<NodeSignature[]>([]);
const [showingSchema, setShowingSchema] = useState<NodeSignature | null>(null);
const [nodes, setNodes] = useState<PConcept[]>([]);
const [loading, setLoading] = useState(true);
const [loadingNodes, setLoadingNodes] = useState(false);
const [error, setError] = useState<string | null>(null);
// Initial load: context, client, schemas, and initial nodes
useEffect(() => {
let cancelled = false;
(async () => {
try {
setLoading(true);
const ctx = await getSupabaseContext();
if (cancelled) return;
setContext(ctx);
if (!ctx) {
setError("No Supabase context available.");
return;
}
const client = await getLoggedInClient();
if (cancelled) return;
if (!client) {
setError("Not logged in to Supabase.");
return;
}
setSupabase(client);
const fetchedSchemas = await getNodeSchemas(client, ctx.spaceId);
if (cancelled) return;
setSchemas(fetchedSchemas);
// Pick first schema by default (if any)
const initialSchema = fetchedSchemas[0] ?? null;
setShowingSchema(initialSchema);
if (initialSchema) {
const initialNodes = await getNodes({
supabase: client,
spaceId: ctx.spaceId,
schemaLocalIds: initialSchema.sourceLocalId,
});
if (cancelled) return;
setNodes(initialNodes);
} else {
// Fallback: load all nodes if no schemas returned
const allNodes = await getNodes({ supabase: client, spaceId: ctx.spaceId });
if (cancelled) return;
setNodes(allNodes);
}
} catch (e) {
console.error("AdminPanel init failed", e);
setError("Failed to initialize Admin Panel.");
} finally {
if (!cancelled) setLoading(false);
}
})();
return () => {
cancelled = true;
};
}, []);
const handleSelectSchema = useCallback(
async (choice: NodeSignature) => {
setShowingSchema(choice);
if (!supabase || !context) return;
try {
setLoadingNodes(true);
const next = await getNodes({
supabase,
spaceId: context.spaceId,
schemaLocalIds: choice.sourceLocalId,
});
setNodes(next);
} catch (e) {
console.error("Failed to load nodes for schema", e);
setError("Failed to load nodes for selected schema.");
} finally {
setLoadingNodes(false);
}
},
[supabase, context],
);
if (loading) {
return (
<div className="p-3">
<Spinner />
<span style={{ marginLeft: 8 }}>Loading admin data…</span>
</div>
);
}
if (error) {
return <p style={{ color: "red" }}>{error}</p>;
}
return (
<div>
<p>
Context:{" "}
<code>
{JSON.stringify(
context ? { ...context, spacePassword: "****" } : null,
)}
</code>
</p>
{schemas.length > 0 ? (
<div>
<div style={{ marginBottom: 12 }}>
<Select<NodeSignature>
items={schemas}
onItemSelect={handleSelectSchema}
itemRenderer={(node, { handleClick, modifiers }) => (
<MenuItem
active={modifiers.active}
key={node.sourceLocalId}
onClick={handleClick}
text={node.name}
/>
)}
filterable={false}
>
<Button
text={
showingSchema ? `display: ${showingSchema.name}` : "Select schema"
}
/>
</Select>
</div>
<div>
{loadingNodes ? (
<div className="p-2">
<Spinner />
<span style={{ marginLeft: 8 }}>Loading nodes…</span>
</div>
) : (
<HTMLTable striped interactive>
<thead>
<tr>
<th>Name</th>
<th>Created</th>
<th>Last Modified</th>
<th>Concept</th>
<th>Content</th>
<th>Document</th>
</tr>
</thead>
<tbody>
{nodes.map((node: PConcept) => (
<tr key={node.id}>
<td>{node.name}</td>
<td>{node.created}</td>
<td>{node.last_modified}</td>
<td>
<pre>{JSON.stringify({ ...node, Content: null }, null, 2)}</pre>
</td>
<td>
<pre>
{JSON.stringify(
{ ...node.Content, Document: null },
null,
2,
)}
</pre>
<span
data-link-title={node.Content?.text}
data-link-uid={node.Content?.source_local_id}
>
<span className="rm-page-ref__brackets">[[</span>
<span
className="rm-page-ref rm-page-ref--link"
onClick={async (event) => {
if (event.shiftKey) {
if (node.Content?.source_local_id) {
await window.roamAlphaAPI.ui.rightSidebar.addWindow(
{
window: {
// @ts-expect-error TODO: fix this
"block-uid": node.Content.source_local_id,
type: "outline",
},
},
);
}
} else if (node.Content?.Document?.source_local_id) {
window.roamAlphaAPI.ui.mainWindow.openPage({
page: { uid: node.Content.Document.source_local_id },
});
}
}}
>
{node.Content?.text}
</span>
<span className="rm-page-ref__brackets">]]</span>
</span>
</td>
<td>
<pre>{JSON.stringify(node.Content?.Document, null, 2)}</pre>
</td>
</tr>
))}
</tbody>
</HTMLTable>
)}
</div>
</div>
) : (
<p>No node schemas found</p>
)}
</div>
);
}
export default AdminPanel;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment