Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0ef2c03
Added 404 & 500 error components to all resources
NuroDev Mar 31, 2026
68040bf
Minor workflow linting fixes
NuroDev Mar 31, 2026
80a36b2
Added changeset
NuroDev Mar 31, 2026
5d31d86
Renamed `RouteError` to `ResourceNotFound`
NuroDev Mar 31, 2026
f2a970e
Merge branch 'main' into NuroDev/local-explorer-not-found
NuroDev Apr 1, 2026
f94611c
Merge branch 'main' into NuroDev/local-explorer-not-found
NuroDev Apr 2, 2026
f485ad0
Moved not found component to shared file
NuroDev Apr 2, 2026
469a326
Merge branch 'main' into NuroDev/local-explorer-not-found
NuroDev Apr 2, 2026
775a7ea
Removed unused imports from app root
NuroDev Apr 2, 2026
ec4ccfc
Improved resource error message handling
NuroDev Apr 2, 2026
9de75b0
Minor `ResourceNotFound` fixes
NuroDev Apr 2, 2026
f939b3d
Overhauled id routes data fetching to correctly redirect not found ro…
NuroDev Apr 2, 2026
8b1a229
Renamed `ResourceNotFound` to `ResourceError`
NuroDev Apr 2, 2026
c6a879b
Merge branch 'main' into NuroDev/local-explorer-not-found
NuroDev Apr 2, 2026
ccd365f
Updated changeset description
NuroDev Apr 2, 2026
d6f0a10
Fixed DO $className not found handling
NuroDev Apr 2, 2026
ed18a45
Merge branch 'main' into NuroDev/local-explorer-not-found
NuroDev Apr 7, 2026
3316ac1
Fixed error details mapping
NuroDev Apr 8, 2026
ff6b02e
Minor code formatting
NuroDev Apr 8, 2026
6e4751d
Merge branch 'main' into NuroDev/local-explorer-not-found
NuroDev Apr 8, 2026
f03d1fe
Merge branch 'main' into NuroDev/local-explorer-not-found
NuroDev Apr 9, 2026
0dec399
Fixed DO table column name
NuroDev Apr 9, 2026
4a5f9c0
Merge branch 'main' into NuroDev/local-explorer-not-found
NuroDev Apr 9, 2026
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
9 changes: 9 additions & 0 deletions .changeset/happy-cameras-occur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@cloudflare/local-explorer-ui": patch
---

Improves local explorer invalid route error handling.

Visiting a route either as a 404 or 500 error now has dedicated components to handle as such, rather than the generic TanStack error UI.

Additionally, it also fixes route loaders to correctly throw a 404 error if a resource is not found, rather than showing a generic error.
19 changes: 19 additions & 0 deletions packages/local-explorer-ui/src/components/NotFound.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Button } from "@cloudflare/kumo";
import { Link, type NotFoundRouteProps } from "@tanstack/react-router";

export function NotFound(_props: NotFoundRouteProps): JSX.Element {
return (
<div className="text-text-secondary flex flex-1 flex-col items-center justify-center space-y-4 p-12 text-center">
<h2 className="text-text text-3xl font-bold">Page not found</h2>

<p className="text-text-secondary text-sm font-light">
The resource you&apos;re looking for doesn&apos;t exist or has been
removed.
</p>

<Link to="/">
<Button variant="secondary">Go home</Button>
</Link>
</div>
);
}
29 changes: 29 additions & 0 deletions packages/local-explorer-ui/src/components/ResourceError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Button } from "@cloudflare/kumo";
import { WarningIcon } from "@phosphor-icons/react";
import { Link, type ErrorComponentProps } from "@tanstack/react-router";
import type { WorkersApiResponseCommonFailure } from "../api";

const DEFAULT_ERROR_DESCRIPTION =
"An unknown error occured. Please report this issue to Cloudflare.";

export function ResourceError({
error,
}: ErrorComponentProps<Error | WorkersApiResponseCommonFailure>): JSX.Element {
const details =
("errors" in error ? error.errors?.[0]?.message : error.message) ??
DEFAULT_ERROR_DESCRIPTION;

return (
<div className="text-text-secondary flex flex-1 flex-col items-center justify-center space-y-4 p-12 text-center">
<WarningIcon size={48} />

<h2 className="text-text text-3xl font-bold">Something went wrong</h2>

<p className="text-text-secondary text-sm font-light">{details}</p>

<Link to="/">
<Button variant="secondary">Go home</Button>
</Link>
</div>
);
}
2 changes: 2 additions & 0 deletions packages/local-explorer-ui/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from "@tanstack/react-router";
import { useCallback, useMemo } from "react";
import { localExplorerListWorkers } from "../api";
import { NotFound } from "../components/NotFound";
import { Sidebar } from "../components/Sidebar";
import {
filterVisibleWorkers,
Expand All @@ -15,6 +16,7 @@ import {

export const Route = createRootRoute({
component: RootLayout,
notFoundComponent: NotFound,
loader: async () => {
const workersResponse = await localExplorerListWorkers();
const workers = workersResponse.data?.result ?? [];
Expand Down
2 changes: 2 additions & 0 deletions packages/local-explorer-ui/src/routes/d1/$databaseId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { useCallback, useMemo, useRef, useState } from "react";
import D1Icon from "../../assets/icons/d1.svg?react";
import { Breadcrumbs } from "../../components/Breadcrumbs";
import { ResourceError } from "../../components/ResourceError";
import { Studio } from "../../components/studio";
import { DropTableConfirmationModal } from "../../components/studio/Modal/DropTableConfirmation";
import { StudioTableActionsDropdown } from "../../components/studio/Table/ActionsDropdown";
Expand All @@ -25,6 +26,7 @@ import type { StudioResource } from "../../types/studio";

export const Route = createFileRoute("/d1/$databaseId")({
component: DatabaseView,
errorComponent: ResourceError,
loader: async (ctx) => {
const driver = new LocalD1Driver(ctx.params.databaseId);
const schemas = await driver.schemas();
Expand Down
8 changes: 6 additions & 2 deletions packages/local-explorer-ui/src/routes/do/$className.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { createFileRoute, Outlet } from "@tanstack/react-router";
import { createFileRoute, notFound, Outlet } from "@tanstack/react-router";
import { durableObjectsNamespaceListNamespaces } from "../../api";
import { NotFound } from "../../components/NotFound";
import { ResourceError } from "../../components/ResourceError";

export const Route = createFileRoute("/do/$className")({
component: () => <Outlet />,
errorComponent: ResourceError,
loader: async ({ params }) => {
const response = await durableObjectsNamespaceListNamespaces();
const namespaces = response.data?.result ?? [];
Expand All @@ -15,12 +18,13 @@ export const Route = createFileRoute("/do/$className")({
ns.id === params.className
);
if (!namespace?.id) {
throw new Error(`Durable Object class "${params.className}" not found`);
throw notFound();
}

return {
className: params.className,
namespaceId: namespace.id,
};
},
notFoundComponent: NotFound,
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ import {
import {
createFileRoute,
Link,
notFound,
useNavigate,
useRouter,
} from "@tanstack/react-router";
import { useCallback, useMemo, useRef, useState } from "react";
import { durableObjectsNamespaceListNamespaces } from "../../../api";
import DOIcon from "../../../assets/icons/durable-objects.svg?react";
import { Breadcrumbs } from "../../../components/Breadcrumbs";
import { NotFound } from "../../../components/NotFound";
import { ResourceError } from "../../../components/ResourceError";
import { Studio } from "../../../components/studio";
import { DropTableConfirmationModal } from "../../../components/studio/Modal/DropTableConfirmation";
import { StudioTableActionsDropdown } from "../../../components/studio/Table/ActionsDropdown";
Expand All @@ -31,6 +34,7 @@ function isHexId(str: string): boolean {

export const Route = createFileRoute("/do/$className/$objectId")({
component: ObjectView,
errorComponent: ResourceError,
loader: async ({ params }) => {
// Resolve className to a namespace ID
const response = await durableObjectsNamespaceListNamespaces();
Expand All @@ -42,7 +46,7 @@ export const Route = createFileRoute("/do/$className/$objectId")({
ns.id === params.className
);
if (!namespace?.id) {
throw new Error(`Durable Object class "${params.className}" not found`);
throw notFound();
}

// Determine if the param is a hex ID or a name
Expand All @@ -67,6 +71,7 @@ export const Route = createFileRoute("/do/$className/$objectId")({
tables,
};
},
notFoundComponent: NotFound,
validateSearch: (search) => ({
table: typeof search.table === "string" ? search.table : undefined,
}),
Expand Down
13 changes: 11 additions & 2 deletions packages/local-explorer-ui/src/routes/do/$className/index.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import { Button, Label, Link as KumoLink, Table } from "@cloudflare/kumo";
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
import {
createFileRoute,
Link,
notFound,
useNavigate,
} from "@tanstack/react-router";
import { useCallback, useEffect, useState } from "react";
import {
durableObjectsNamespaceListNamespaces,
durableObjectsNamespaceListObjects,
} from "../../../api";
import DOIcon from "../../../assets/icons/durable-objects.svg?react";
import { Breadcrumbs } from "../../../components/Breadcrumbs";
import { NotFound } from "../../../components/NotFound";
import { ResourceError } from "../../../components/ResourceError";
import type { WorkersObject } from "../../../api";

export const Route = createFileRoute("/do/$className/")({
component: NamespaceView,
errorComponent: ResourceError,
loader: async ({ params }) => {
const response = await durableObjectsNamespaceListNamespaces();
const namespaces = response.data?.result ?? [];
Expand All @@ -21,7 +29,7 @@ export const Route = createFileRoute("/do/$className/")({
ns.id === params.className
);
if (!namespace?.id) {
throw new Error(`Durable Object class "${params.className}" not found`);
throw notFound();
}

const objectsResponse = await durableObjectsNamespaceListObjects({
Expand All @@ -43,6 +51,7 @@ export const Route = createFileRoute("/do/$className/")({
objects,
};
},
notFoundComponent: NotFound,
});

function NamespaceView() {
Expand Down
2 changes: 2 additions & 0 deletions packages/local-explorer-ui/src/routes/kv/$namespaceId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ import KVIcon from "../../assets/icons/kv.svg?react";
import { AddKVForm } from "../../components/AddKVForm";
import { Breadcrumbs } from "../../components/Breadcrumbs";
import { KVTable } from "../../components/KVTable";
import { ResourceError } from "../../components/ResourceError";
import { SearchForm } from "../../components/SearchForm";
import { getSelectedWorker } from "../../components/WorkerSelector";
import type { KVEntry } from "../../api";

export const Route = createFileRoute("/kv/$namespaceId")({
component: NamespaceView,
errorComponent: ResourceError,
loader: async ({ params }) => {
const keysResponse = await workersKvNamespaceListANamespace_SKeys({
path: { namespace_id: params.namespaceId },
Expand Down
2 changes: 2 additions & 0 deletions packages/local-explorer-ui/src/routes/r2/$bucketName.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { createFileRoute, Outlet } from "@tanstack/react-router";
import { ResourceError } from "../../components/ResourceError";

export const Route = createFileRoute("/r2/$bucketName")({
component: () => <Outlet />,
errorComponent: ResourceError,
});
29 changes: 24 additions & 5 deletions packages/local-explorer-ui/src/routes/r2/$bucketName/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ import {
ListIcon,
UploadIcon,
} from "@phosphor-icons/react";
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
import {
createFileRoute,
Link,
notFound,
useNavigate,
} from "@tanstack/react-router";
import { useCallback, useEffect, useState } from "react";
import {
r2BucketDeleteObjects,
Expand All @@ -22,8 +27,10 @@ import {
} from "../../../api";
import R2Icon from "../../../assets/icons/r2.svg?react";
import { Breadcrumbs } from "../../../components/Breadcrumbs";
import { NotFound } from "../../../components/NotFound";
import { R2ObjectTable } from "../../../components/R2ObjectTable";
import { R2UploadDialog } from "../../../components/R2UploadDialog";
import { ResourceError } from "../../../components/ResourceError";
import { withMinimumDelay } from "../../../utils/async";
import type { R2Object } from "../../../api";

Expand All @@ -34,10 +41,7 @@ export interface R2BucketSearch {

export const Route = createFileRoute("/r2/$bucketName/")({
component: BucketView,
validateSearch: (search: Record<string, unknown>): R2BucketSearch => ({
prefix: typeof search.prefix === "string" ? search.prefix : undefined,
delimiter: search.delimiter === false ? false : true,
}),
errorComponent: ResourceError,
loaderDeps: ({ search }) => ({
prefix: search.prefix,
delimiter: search.delimiter,
Expand All @@ -52,7 +56,17 @@ export const Route = createFileRoute("/r2/$bucketName/")({
per_page: 100,
prefix: deps.prefix || undefined,
},
throwOnError: false,
});
if (response.response?.status === 404) {
throw notFound();
}

if (response.error) {
throw new Error(
`Failed to list objects in bucket "${params.bucketName}"`
);
}

return {
objects: response.data?.result ?? [],
Expand All @@ -62,6 +76,11 @@ export const Route = createFileRoute("/r2/$bucketName/")({
delimiterEnabled: deps.delimiter !== false,
};
},
notFoundComponent: NotFound,
validateSearch: (search: Record<string, unknown>): R2BucketSearch => ({
prefix: typeof search.prefix === "string" ? search.prefix : undefined,
delimiter: search.delimiter === false ? false : true,
Comment thread
NuroDev marked this conversation as resolved.
}),
});

function BucketView(): JSX.Element {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import { Button, Dialog } from "@cloudflare/kumo";
import { DownloadIcon, TrashIcon } from "@phosphor-icons/react";
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
import {
createFileRoute,
Link,
notFound,
useNavigate,
} from "@tanstack/react-router";
import { useState } from "react";
import { r2BucketDeleteObjects, r2BucketGetObject } from "../../../api";
import R2Icon from "../../../assets/icons/r2.svg?react";
import { Breadcrumbs } from "../../../components/Breadcrumbs";
import { CopyButton } from "../../../components/CopyButton";
import { NotFound } from "../../../components/NotFound";
import { ResourceError } from "../../../components/ResourceError";
import { formatDate, formatSize } from "../../../utils/format";
import type { R2HeadObjectResult } from "../../../api";

export const Route = createFileRoute("/r2/$bucketName/object/$")({
component: ObjectDetailView,
errorComponent: ResourceError,
loader: async ({ params }) => {
const objectKey = params._splat;
if (!objectKey) {
Expand All @@ -25,18 +33,27 @@ export const Route = createFileRoute("/r2/$bucketName/object/$")({
headers: {
"cf-metadata-only": "true",
},
throwOnError: false,
});
if (response.response?.status === 404) {
throw notFound();
}

if (response.error) {
throw new Error(`Failed to fetch object "${objectKey}"`);
}

const result = response.data?.result;
if (!result) {
throw new Error(`Object "${objectKey}" not found`);
throw notFound();
}

return {
object: result,
objectKey,
};
},
notFoundComponent: NotFound,
});

interface ObjectDetailsCardProps {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { createFileRoute, Outlet } from "@tanstack/react-router";
import { ResourceError } from "../../components/ResourceError";

export const Route = createFileRoute("/workflows/$workflowName")({
component: () => <Outlet />,
errorComponent: ResourceError,
loader: async ({ params }) => {
return {
workflowName: params.workflowName,
Expand Down
Loading
Loading