Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/thirty-mammals-live.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@cloudflare/local-explorer-ui": minor
---

Refactors KV & sidebar to use route loaders.

This change improves the user experience of the Local Explorer dashboard by ensuring that the data used for the initial render is fetched server-side and passed down to the client. This avoids the initial flicker when loading in. Both D1 & Durable Object routes already incorporate this system.
17 changes: 2 additions & 15 deletions packages/local-explorer-ui/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ interface SidebarItemGroupProps {
to: FileRouteTypes["to"];
};
}>;
loading: boolean;
title: string;
}

Expand All @@ -35,7 +34,6 @@ function SidebarItemGroup({
error,
icon: Icon,
items,
loading,
title,
}: SidebarItemGroupProps): JSX.Element {
return (
Expand All @@ -51,17 +49,11 @@ function SidebarItemGroup({

<Collapsible.Panel className="overflow-hidden transition-[height,opacity] duration-200 ease-out data-starting-style:h-0 data-starting-style:opacity-0 data-ending-style:h-0 data-ending-style:opacity-0">
<ul className="list-none ml-3 pl-3 space-y-0.5 border-l border-border">
{loading ? (
<li className="py-1.5 px-2 text-text-secondary text-sm">
Loading...
</li>
) : null}

{error ? (
<li className="py-1.5 px-2 text-danger text-sm">{error}</li>
) : null}

{!loading && !error
{!error
? items.map((item) => (
<li key={item.id}>
<Link
Expand All @@ -81,7 +73,7 @@ function SidebarItemGroup({
))
: null}

{!loading && !error && items.length === 0 && (
{!error && items.length === 0 && (
<li className="py-1.5 px-2 text-text-secondary text-sm italic">
{emptyLabel}
</li>
Expand All @@ -100,7 +92,6 @@ interface SidebarProps {
doNamespaces: WorkersNamespace[];
kvError: string | null;
kvNamespaces: WorkersKvNamespace[];
loading: boolean;
}

export function Sidebar({
Expand All @@ -111,7 +102,6 @@ export function Sidebar({
doNamespaces,
kvError,
kvNamespaces,
loading,
}: SidebarProps) {
return (
<aside className="w-sidebar bg-bg-secondary border-r border-border flex flex-col">
Expand Down Expand Up @@ -143,7 +133,6 @@ export function Sidebar({
to: "/kv/$namespaceId",
},
}))}
loading={loading}
title="KV Namespaces"
/>

Expand All @@ -161,7 +150,6 @@ export function Sidebar({
to: "/d1/$databaseId",
},
}))}
loading={loading}
title="D1 Databases"
/>

Expand All @@ -183,7 +171,6 @@ export function Sidebar({
},
};
})}
loading={loading}
title="Durable Objects"
/>
</aside>
Expand Down
103 changes: 47 additions & 56 deletions packages/local-explorer-ui/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
Outlet,
useRouterState,
} from "@tanstack/react-router";
import { useEffect, useState } from "react";
import {
cloudflareD1ListDatabases,
durableObjectsNamespaceListNamespaces,
Expand All @@ -18,73 +17,65 @@ import type {

export const Route = createRootRoute({
component: RootLayout,
});
loader: async () => {
const [kvResponse, d1Response, doResponse] = await Promise.allSettled([
workersKvNamespaceListNamespaces(),
cloudflareD1ListDatabases(),
durableObjectsNamespaceListNamespaces(),
]);

function RootLayout() {
const [loading, setLoading] = useState<boolean>(true);
let kvNamespaces = new Array<WorkersKvNamespace>();
let kvError: string | null = null;
if (kvResponse.status === "fulfilled") {
kvNamespaces = kvResponse.value.data?.result ?? [];
} else {
kvError = `KV Error: ${kvResponse.reason instanceof Error ? kvResponse.reason.message : JSON.stringify(kvResponse.reason)}`;
}

const [kvNamespaces, setKvNamespaces] = useState<WorkersKvNamespace[]>([]);
const [kvError, setKvError] = useState<string | null>(null);
let databases = new Array<D1DatabaseResponse>();
let d1Error: string | null = null;
if (d1Response.status === "fulfilled") {
databases = d1Response.value.data?.result ?? [];
} else {
d1Error = `D1 Error: ${d1Response.reason instanceof Error ? d1Response.reason.message : JSON.stringify(d1Response.reason)}`;
}

const [d1Error, setD1Error] = useState<string | null>(null);
const [databases, setDatabases] = useState<D1DatabaseResponse[]>([]);
let doNamespaces = new Array<WorkersNamespace>();
let doError: string | null = null;
if (doResponse.status === "fulfilled") {
// Only show namespaces that use SQLite storage
const allDoNamespaces = doResponse.value.data?.result ?? [];
doNamespaces = allDoNamespaces.filter((ns) => ns.use_sqlite === true);
} else {
doError = `DO Error: ${doResponse.reason instanceof Error ? doResponse.reason.message : JSON.stringify(doResponse.reason)}`;
}

const [doNamespaces, setDoNamespaces] = useState<WorkersNamespace[]>([]);
const [doError, setDoError] = useState<string | null>(null);
return {
d1Error,
databases,
doError,
doNamespaces,
kvError,
kvNamespaces,
};
},
});

function RootLayout() {
const loaderData = Route.useLoaderData();
const routerState = useRouterState();
const currentPath = routerState.location.pathname;

useEffect(() => {
async function fetchData() {
const [kvResponse, d1Response, doResponse] = await Promise.allSettled([
workersKvNamespaceListNamespaces(),
cloudflareD1ListDatabases(),
durableObjectsNamespaceListNamespaces(),
]);

if (kvResponse.status === "fulfilled") {
setKvNamespaces(kvResponse.value.data?.result ?? []);
} else {
setKvError(
`KV Error: ${kvResponse.reason instanceof Error ? kvResponse.reason.message : JSON.stringify(kvResponse.reason)}`
);
}

if (d1Response.status === "fulfilled") {
setDatabases(d1Response.value.data?.result ?? []);
} else {
setD1Error(
`D1 Error: ${d1Response.reason instanceof Error ? d1Response.reason.message : JSON.stringify(d1Response.reason)}`
);
}

if (doResponse.status === "fulfilled") {
// Only show namespaces that use SQLite storage
const allDoNamespaces = doResponse.value.data?.result ?? [];
setDoNamespaces(allDoNamespaces.filter((ns) => ns.use_sqlite === true));
} else {
setDoError(
`DO Error: ${doResponse.reason instanceof Error ? doResponse.reason.message : JSON.stringify(doResponse.reason)}`
);
}

setLoading(false);
}
void fetchData();
}, []);

return (
<div className="flex min-h-screen">
<Sidebar
currentPath={currentPath}
d1Error={d1Error}
databases={databases}
doError={doError}
doNamespaces={doNamespaces}
kvError={kvError}
kvNamespaces={kvNamespaces}
loading={loading}
d1Error={loaderData.d1Error}
databases={loaderData.databases}
doError={loaderData.doError}
doNamespaces={loaderData.doNamespaces}
kvError={loaderData.kvError}
kvNamespaces={loaderData.kvNamespaces}
/>
<main className="flex-1 overflow-y-auto flex flex-col">
<Outlet />
Expand Down
38 changes: 28 additions & 10 deletions packages/local-explorer-ui/src/routes/do/$className/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import type { WorkersObject } from "../../../api";
export const Route = createFileRoute("/do/$className/")({
component: NamespaceView,
loader: async ({ params }) => {
// Resolve className to namespaceId
const response = await durableObjectsNamespaceListNamespaces();
const namespaces = response.data?.result ?? [];
const namespace = namespaces.find(
Expand All @@ -25,8 +24,23 @@ export const Route = createFileRoute("/do/$className/")({
throw new Error(`Durable Object class "${params.className}" not found`);
}

const objectsResponse = await durableObjectsNamespaceListObjects({
path: {
id: namespace.id,
},
query: {
limit: 50,
},
});

const objects = objectsResponse.data?.result ?? [];
const cursor = objectsResponse.data?.result_info?.cursor ?? null;

return {
cursor,
hasMore: !!cursor,
namespaceId: namespace.id,
objects,
};
},
});
Expand All @@ -36,12 +50,21 @@ function NamespaceView() {
const loaderData = Route.useLoaderData();
const { namespaceId } = loaderData;

const [cursor, setCursor] = useState<string | null>(null);
const [cursor, setCursor] = useState<string | null>(loaderData.cursor);
const [error, setError] = useState<string | null>(null);
const [hasMore, setHasMore] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(true);
const [hasMore, setHasMore] = useState<boolean>(loaderData.hasMore);
const [loading, setLoading] = useState<boolean>(false);
const [loadingMore, setLoadingMore] = useState<boolean>(false);
const [objects, setObjects] = useState<WorkersObject[]>([]);
const [objects, setObjects] = useState<WorkersObject[]>(loaderData.objects);

useEffect((): void => {
setObjects(loaderData.objects);
setCursor(loaderData.cursor);
setHasMore(loaderData.hasMore);
setError(null);
setLoading(false);
setLoadingMore(false);
}, [loaderData]);

const fetchObjects = useCallback(
async (nextCursor?: string): Promise<void> => {
Expand Down Expand Up @@ -87,11 +110,6 @@ function NamespaceView() {
[namespaceId]
);

useEffect((): void => {
setError(null);
void fetchObjects();
}, [namespaceId, fetchObjects]);

function handleLoadMore(): void {
if (cursor && !loadingMore) {
void fetchObjects(cursor);
Expand Down
Loading
Loading