From 0ef2c039de8f9c67fc934b06a2f277c66e8f839f Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Wed, 1 Apr 2026 00:06:43 +0100 Subject: [PATCH 01/15] Added 404 & 500 error components to all resources --- .../src/components/ResourceNotFound.tsx | 18 ++++++++++++++++++ .../local-explorer-ui/src/routes/__root.tsx | 17 ++++++++++++++++- .../src/routes/d1/$databaseId.tsx | 2 ++ .../src/routes/do/$className.tsx | 2 ++ .../src/routes/do/$className/$objectId.tsx | 2 ++ .../src/routes/do/$className/index.tsx | 2 ++ .../src/routes/kv/$namespaceId.tsx | 2 ++ .../src/routes/r2/$bucketName.tsx | 2 ++ .../src/routes/r2/$bucketName/index.tsx | 10 ++++++---- .../src/routes/r2/$bucketName/object.$.tsx | 2 ++ .../src/routes/workflows/$workflowName.tsx | 2 ++ .../workflows/$workflowName/$instanceId.tsx | 2 ++ .../routes/workflows/$workflowName/index.tsx | 2 ++ 13 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 packages/local-explorer-ui/src/components/ResourceNotFound.tsx diff --git a/packages/local-explorer-ui/src/components/ResourceNotFound.tsx b/packages/local-explorer-ui/src/components/ResourceNotFound.tsx new file mode 100644 index 0000000000..6eaf23d0ae --- /dev/null +++ b/packages/local-explorer-ui/src/components/ResourceNotFound.tsx @@ -0,0 +1,18 @@ +import { Button } from "@cloudflare/kumo"; +import { Link, type ErrorComponentProps } from "@tanstack/react-router"; + +export function RouteError(_props: ErrorComponentProps): JSX.Element { + return ( +
+

Resource not found

+ +

+ This binding doesn't exist in your current dev session. +

+ + + + +
+ ); +} diff --git a/packages/local-explorer-ui/src/routes/__root.tsx b/packages/local-explorer-ui/src/routes/__root.tsx index ba77c16455..ac548900f9 100644 --- a/packages/local-explorer-ui/src/routes/__root.tsx +++ b/packages/local-explorer-ui/src/routes/__root.tsx @@ -1,6 +1,7 @@ -import { Toasty } from "@cloudflare/kumo"; +import { Button, Toasty } from "@cloudflare/kumo"; import { createRootRoute, + Link, Outlet, useRouter, useRouterState, @@ -33,6 +34,20 @@ type R2BucketWithWorker = R2Bucket & { workerName?: string }; export const Route = createRootRoute({ component: RootLayout, + notFoundComponent: () => ( +
+

Page not found

+ +

+ The resource you're looking for doesn't exist or has been + removed. +

+ + + + +
+ ), loader: async () => { const [ workersResponse, diff --git a/packages/local-explorer-ui/src/routes/d1/$databaseId.tsx b/packages/local-explorer-ui/src/routes/d1/$databaseId.tsx index ff667df426..9d0468bf89 100644 --- a/packages/local-explorer-ui/src/routes/d1/$databaseId.tsx +++ b/packages/local-explorer-ui/src/routes/d1/$databaseId.tsx @@ -13,6 +13,7 @@ import { import { useCallback, useMemo, useRef, useState } from "react"; import D1Icon from "../../assets/icons/d1.svg?react"; import { Breadcrumbs } from "../../components/Breadcrumbs"; +import { RouteError } from "../../components/ResourceNotFound"; import { Studio } from "../../components/studio"; import { DropTableConfirmationModal } from "../../components/studio/Modal/DropTableConfirmation"; import { StudioTableActionsDropdown } from "../../components/studio/Table/ActionsDropdown"; @@ -23,6 +24,7 @@ import type { StudioResource } from "../../types/studio"; export const Route = createFileRoute("/d1/$databaseId")({ component: DatabaseView, + errorComponent: RouteError, loader: async (ctx) => { const driver = new LocalD1Driver(ctx.params.databaseId); const schemas = await driver.schemas(); diff --git a/packages/local-explorer-ui/src/routes/do/$className.tsx b/packages/local-explorer-ui/src/routes/do/$className.tsx index 2482492fb4..5bdf4de7ed 100644 --- a/packages/local-explorer-ui/src/routes/do/$className.tsx +++ b/packages/local-explorer-ui/src/routes/do/$className.tsx @@ -1,8 +1,10 @@ import { createFileRoute, Outlet } from "@tanstack/react-router"; import { durableObjectsNamespaceListNamespaces } from "../../api"; +import { RouteError } from "../../components/ResourceNotFound"; export const Route = createFileRoute("/do/$className")({ component: () => , + errorComponent: RouteError, loader: async ({ params }) => { const response = await durableObjectsNamespaceListNamespaces(); const namespaces = response.data?.result ?? []; diff --git a/packages/local-explorer-ui/src/routes/do/$className/$objectId.tsx b/packages/local-explorer-ui/src/routes/do/$className/$objectId.tsx index 7f382890c1..0f573a5a2d 100644 --- a/packages/local-explorer-ui/src/routes/do/$className/$objectId.tsx +++ b/packages/local-explorer-ui/src/routes/do/$className/$objectId.tsx @@ -14,6 +14,7 @@ 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 { RouteError } from "../../../components/ResourceNotFound"; import { Studio } from "../../../components/studio"; import { DropTableConfirmationModal } from "../../../components/studio/Modal/DropTableConfirmation"; import { StudioTableActionsDropdown } from "../../../components/studio/Table/ActionsDropdown"; @@ -24,6 +25,7 @@ import type { StudioResource } from "../../../types/studio"; export const Route = createFileRoute("/do/$className/$objectId")({ component: ObjectView, + errorComponent: RouteError, loader: async ({ params }) => { // Resolve className to a namespace ID const response = await durableObjectsNamespaceListNamespaces(); diff --git a/packages/local-explorer-ui/src/routes/do/$className/index.tsx b/packages/local-explorer-ui/src/routes/do/$className/index.tsx index ec3ce1b077..594f71ea26 100644 --- a/packages/local-explorer-ui/src/routes/do/$className/index.tsx +++ b/packages/local-explorer-ui/src/routes/do/$className/index.tsx @@ -7,10 +7,12 @@ import { } from "../../../api"; import DOIcon from "../../../assets/icons/durable-objects.svg?react"; import { Breadcrumbs } from "../../../components/Breadcrumbs"; +import { RouteError } from "../../../components/ResourceNotFound"; import type { WorkersObject } from "../../../api"; export const Route = createFileRoute("/do/$className/")({ component: NamespaceView, + errorComponent: RouteError, loader: async ({ params }) => { const response = await durableObjectsNamespaceListNamespaces(); const namespaces = response.data?.result ?? []; diff --git a/packages/local-explorer-ui/src/routes/kv/$namespaceId.tsx b/packages/local-explorer-ui/src/routes/kv/$namespaceId.tsx index 3873ce43c0..e309286f55 100644 --- a/packages/local-explorer-ui/src/routes/kv/$namespaceId.tsx +++ b/packages/local-explorer-ui/src/routes/kv/$namespaceId.tsx @@ -12,11 +12,13 @@ import KVIcon from "../../assets/icons/kv.svg?react"; import { AddKVForm } from "../../components/AddKVForm"; import { Breadcrumbs } from "../../components/Breadcrumbs"; import { KVTable } from "../../components/KVTable"; +import { RouteError } from "../../components/ResourceNotFound"; import { SearchForm } from "../../components/SearchForm"; import type { KVEntry } from "../../api"; export const Route = createFileRoute("/kv/$namespaceId")({ component: NamespaceView, + errorComponent: RouteError, loader: async ({ params }) => { const keysResponse = await workersKvNamespaceListANamespace_SKeys({ path: { namespace_id: params.namespaceId }, diff --git a/packages/local-explorer-ui/src/routes/r2/$bucketName.tsx b/packages/local-explorer-ui/src/routes/r2/$bucketName.tsx index 72c633d063..e0badc8f44 100644 --- a/packages/local-explorer-ui/src/routes/r2/$bucketName.tsx +++ b/packages/local-explorer-ui/src/routes/r2/$bucketName.tsx @@ -1,5 +1,7 @@ import { createFileRoute, Outlet } from "@tanstack/react-router"; +import { RouteError } from "../../components/ResourceNotFound"; export const Route = createFileRoute("/r2/$bucketName")({ component: () => , + errorComponent: RouteError, }); diff --git a/packages/local-explorer-ui/src/routes/r2/$bucketName/index.tsx b/packages/local-explorer-ui/src/routes/r2/$bucketName/index.tsx index b9a95e9711..7733b94e70 100644 --- a/packages/local-explorer-ui/src/routes/r2/$bucketName/index.tsx +++ b/packages/local-explorer-ui/src/routes/r2/$bucketName/index.tsx @@ -24,6 +24,7 @@ import R2Icon from "../../../assets/icons/r2.svg?react"; import { Breadcrumbs } from "../../../components/Breadcrumbs"; import { R2ObjectTable } from "../../../components/R2ObjectTable"; import { R2UploadDialog } from "../../../components/R2UploadDialog"; +import { RouteError } from "../../../components/ResourceNotFound"; import { withMinimumDelay } from "../../../utils/async"; import type { R2Object } from "../../../api"; @@ -34,10 +35,7 @@ export interface R2BucketSearch { export const Route = createFileRoute("/r2/$bucketName/")({ component: BucketView, - validateSearch: (search: Record): R2BucketSearch => ({ - prefix: typeof search.prefix === "string" ? search.prefix : undefined, - delimiter: search.delimiter === false ? false : true, - }), + errorComponent: RouteError, loaderDeps: ({ search }) => ({ prefix: search.prefix, delimiter: search.delimiter, @@ -62,6 +60,10 @@ export const Route = createFileRoute("/r2/$bucketName/")({ delimiterEnabled: deps.delimiter !== false, }; }, + validateSearch: (search: Record): R2BucketSearch => ({ + prefix: typeof search.prefix === "string" ? search.prefix : undefined, + delimiter: search.delimiter === false ? false : true, + }), }); function BucketView(): JSX.Element { diff --git a/packages/local-explorer-ui/src/routes/r2/$bucketName/object.$.tsx b/packages/local-explorer-ui/src/routes/r2/$bucketName/object.$.tsx index 35866d7cf5..e82339b8a0 100644 --- a/packages/local-explorer-ui/src/routes/r2/$bucketName/object.$.tsx +++ b/packages/local-explorer-ui/src/routes/r2/$bucketName/object.$.tsx @@ -6,11 +6,13 @@ import { r2BucketDeleteObjects, r2BucketGetObject } from "../../../api"; import R2Icon from "../../../assets/icons/r2.svg?react"; import { Breadcrumbs } from "../../../components/Breadcrumbs"; import { CopyButton } from "../../../components/CopyButton"; +import { RouteError } from "../../../components/ResourceNotFound"; import { formatDate, formatSize } from "../../../utils/format"; import type { R2HeadObjectResult } from "../../../api"; export const Route = createFileRoute("/r2/$bucketName/object/$")({ component: ObjectDetailView, + errorComponent: RouteError, loader: async ({ params }) => { const objectKey = params._splat; if (!objectKey) { diff --git a/packages/local-explorer-ui/src/routes/workflows/$workflowName.tsx b/packages/local-explorer-ui/src/routes/workflows/$workflowName.tsx index f22d66f298..d12ac1cdbf 100644 --- a/packages/local-explorer-ui/src/routes/workflows/$workflowName.tsx +++ b/packages/local-explorer-ui/src/routes/workflows/$workflowName.tsx @@ -1,7 +1,9 @@ import { createFileRoute, Outlet } from "@tanstack/react-router"; +import { RouteError } from "../../components/ResourceNotFound"; export const Route = createFileRoute("/workflows/$workflowName")({ component: () => , + errorComponent: RouteError, loader: async ({ params }) => { return { workflowName: params.workflowName, diff --git a/packages/local-explorer-ui/src/routes/workflows/$workflowName/$instanceId.tsx b/packages/local-explorer-ui/src/routes/workflows/$workflowName/$instanceId.tsx index bcd38577e6..a9cb5b612e 100644 --- a/packages/local-explorer-ui/src/routes/workflows/$workflowName/$instanceId.tsx +++ b/packages/local-explorer-ui/src/routes/workflows/$workflowName/$instanceId.tsx @@ -30,6 +30,7 @@ import { } from "../../../api"; import WorkflowsIcon from "../../../assets/icons/workflows.svg?react"; import { Breadcrumbs } from "../../../components/Breadcrumbs"; +import { RouteError } from "../../../components/ResourceNotFound"; import { CopyButton } from "../../../components/workflows/CopyButton"; import { formatDuration, @@ -50,6 +51,7 @@ import type { export const Route = createFileRoute("/workflows/$workflowName/$instanceId")({ component: InstanceDetailView, + errorComponent: RouteError, loader: async ({ params }) => { const response = await workflowsGetInstanceDetails({ path: { diff --git a/packages/local-explorer-ui/src/routes/workflows/$workflowName/index.tsx b/packages/local-explorer-ui/src/routes/workflows/$workflowName/index.tsx index 5b2fdd8d89..ee07270c83 100644 --- a/packages/local-explorer-ui/src/routes/workflows/$workflowName/index.tsx +++ b/packages/local-explorer-ui/src/routes/workflows/$workflowName/index.tsx @@ -32,6 +32,7 @@ import { } from "../../../api"; import WorkflowsIcon from "../../../assets/icons/workflows.svg?react"; import { Breadcrumbs } from "../../../components/Breadcrumbs"; +import { RouteError } from "../../../components/ResourceNotFound"; import { CreateWorkflowInstanceDialog } from "../../../components/workflows/CreateInstanceDialog"; import { timeAgo } from "../../../components/workflows/helpers"; import { WorkflowStatusBadge } from "../../../components/workflows/StatusBadge"; @@ -41,6 +42,7 @@ import type { Action } from "../../../components/workflows/types"; export const Route = createFileRoute("/workflows/$workflowName/")({ component: WorkflowInstancesView, + errorComponent: RouteError, loader: async ({ params }) => { const response = await workflowsListInstances({ path: { workflow_name: params.workflowName }, From 68040bfca7909d78774430e179affa214254e806 Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Wed, 1 Apr 2026 00:06:49 +0100 Subject: [PATCH 02/15] Minor workflow linting fixes --- .../src/routes/workflows/$workflowName/$instanceId.tsx | 10 +++++----- .../src/routes/workflows/$workflowName/index.tsx | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/local-explorer-ui/src/routes/workflows/$workflowName/$instanceId.tsx b/packages/local-explorer-ui/src/routes/workflows/$workflowName/$instanceId.tsx index a9cb5b612e..3f2327c07f 100644 --- a/packages/local-explorer-ui/src/routes/workflows/$workflowName/$instanceId.tsx +++ b/packages/local-explorer-ui/src/routes/workflows/$workflowName/$instanceId.tsx @@ -166,11 +166,11 @@ const ErrorCard = memo(function ErrorCard({ }) { return ( - + {error.name ?? "Error"} - -
+			
+				
 					{error.message ?? "Unknown error"}
 				
@@ -488,7 +488,7 @@ function InstanceDetailView() { {/* Delete confirmation dialog */} - +
{/* @ts-expect-error - Type mismatch due to pnpm monorepo @types/react version conflict */} @@ -556,7 +556,7 @@ function InstanceDetailView() { } }} > - +
{/* @ts-expect-error - Type mismatch due to pnpm monorepo @types/react version conflict */} diff --git a/packages/local-explorer-ui/src/routes/workflows/$workflowName/index.tsx b/packages/local-explorer-ui/src/routes/workflows/$workflowName/index.tsx index ee07270c83..d712a3348e 100644 --- a/packages/local-explorer-ui/src/routes/workflows/$workflowName/index.tsx +++ b/packages/local-explorer-ui/src/routes/workflows/$workflowName/index.tsx @@ -407,7 +407,7 @@ const InstanceRow = memo(function InstanceRow({ } }} > - +
{/* @ts-expect-error - Type mismatch due to pnpm monorepo @types/react version conflict */} @@ -529,7 +529,7 @@ function SettingsTab({
- +
{/* @ts-expect-error - Type mismatch due to pnpm monorepo @types/react version conflict */} From 80a36b24b8ae4b16c9661d9303177978d5ae2fcb Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Wed, 1 Apr 2026 00:13:59 +0100 Subject: [PATCH 03/15] Added changeset --- .changeset/happy-cameras-occur.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/happy-cameras-occur.md diff --git a/.changeset/happy-cameras-occur.md b/.changeset/happy-cameras-occur.md new file mode 100644 index 0000000000..6b041e28ea --- /dev/null +++ b/.changeset/happy-cameras-occur.md @@ -0,0 +1,9 @@ +--- +"@cloudflare/local-explorer-ui": patch +--- + +Fix local explorer invalid route messages. + +When trying to access a route, like `/foo`, that doesn't exist we show a custom 404 error message with a redirect button. + +Similarly we do the same for when trying to access a binding resource that doesn't exist. Now showing a "Resource not found" error with a redirect button. From 5d31d8682eab0cdcd066ede0465a53cab0c89287 Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Wed, 1 Apr 2026 00:14:34 +0100 Subject: [PATCH 04/15] Renamed `RouteError` to `ResourceNotFound` --- .../local-explorer-ui/src/components/ResourceNotFound.tsx | 2 +- packages/local-explorer-ui/src/routes/d1/$databaseId.tsx | 4 ++-- packages/local-explorer-ui/src/routes/do/$className.tsx | 4 ++-- .../local-explorer-ui/src/routes/do/$className/$objectId.tsx | 4 ++-- packages/local-explorer-ui/src/routes/do/$className/index.tsx | 4 ++-- packages/local-explorer-ui/src/routes/kv/$namespaceId.tsx | 4 ++-- packages/local-explorer-ui/src/routes/r2/$bucketName.tsx | 4 ++-- .../local-explorer-ui/src/routes/r2/$bucketName/index.tsx | 4 ++-- .../local-explorer-ui/src/routes/r2/$bucketName/object.$.tsx | 4 ++-- .../local-explorer-ui/src/routes/workflows/$workflowName.tsx | 4 ++-- .../src/routes/workflows/$workflowName/$instanceId.tsx | 4 ++-- .../src/routes/workflows/$workflowName/index.tsx | 4 ++-- 12 files changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/local-explorer-ui/src/components/ResourceNotFound.tsx b/packages/local-explorer-ui/src/components/ResourceNotFound.tsx index 6eaf23d0ae..cb66e100d8 100644 --- a/packages/local-explorer-ui/src/components/ResourceNotFound.tsx +++ b/packages/local-explorer-ui/src/components/ResourceNotFound.tsx @@ -1,7 +1,7 @@ import { Button } from "@cloudflare/kumo"; import { Link, type ErrorComponentProps } from "@tanstack/react-router"; -export function RouteError(_props: ErrorComponentProps): JSX.Element { +export function ResourceNotFound(_props: ErrorComponentProps): JSX.Element { return (

Resource not found

diff --git a/packages/local-explorer-ui/src/routes/d1/$databaseId.tsx b/packages/local-explorer-ui/src/routes/d1/$databaseId.tsx index 9d0468bf89..76b9aef007 100644 --- a/packages/local-explorer-ui/src/routes/d1/$databaseId.tsx +++ b/packages/local-explorer-ui/src/routes/d1/$databaseId.tsx @@ -13,7 +13,7 @@ import { import { useCallback, useMemo, useRef, useState } from "react"; import D1Icon from "../../assets/icons/d1.svg?react"; import { Breadcrumbs } from "../../components/Breadcrumbs"; -import { RouteError } from "../../components/ResourceNotFound"; +import { ResourceNotFound } from "../../components/ResourceNotFound"; import { Studio } from "../../components/studio"; import { DropTableConfirmationModal } from "../../components/studio/Modal/DropTableConfirmation"; import { StudioTableActionsDropdown } from "../../components/studio/Table/ActionsDropdown"; @@ -24,7 +24,7 @@ import type { StudioResource } from "../../types/studio"; export const Route = createFileRoute("/d1/$databaseId")({ component: DatabaseView, - errorComponent: RouteError, + errorComponent: ResourceNotFound, loader: async (ctx) => { const driver = new LocalD1Driver(ctx.params.databaseId); const schemas = await driver.schemas(); diff --git a/packages/local-explorer-ui/src/routes/do/$className.tsx b/packages/local-explorer-ui/src/routes/do/$className.tsx index 5bdf4de7ed..c3b2b71f29 100644 --- a/packages/local-explorer-ui/src/routes/do/$className.tsx +++ b/packages/local-explorer-ui/src/routes/do/$className.tsx @@ -1,10 +1,10 @@ import { createFileRoute, Outlet } from "@tanstack/react-router"; import { durableObjectsNamespaceListNamespaces } from "../../api"; -import { RouteError } from "../../components/ResourceNotFound"; +import { ResourceNotFound } from "../../components/ResourceNotFound"; export const Route = createFileRoute("/do/$className")({ component: () => , - errorComponent: RouteError, + errorComponent: ResourceNotFound, loader: async ({ params }) => { const response = await durableObjectsNamespaceListNamespaces(); const namespaces = response.data?.result ?? []; diff --git a/packages/local-explorer-ui/src/routes/do/$className/$objectId.tsx b/packages/local-explorer-ui/src/routes/do/$className/$objectId.tsx index 0f573a5a2d..9e713b7aab 100644 --- a/packages/local-explorer-ui/src/routes/do/$className/$objectId.tsx +++ b/packages/local-explorer-ui/src/routes/do/$className/$objectId.tsx @@ -14,7 +14,7 @@ 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 { RouteError } from "../../../components/ResourceNotFound"; +import { ResourceNotFound } from "../../../components/ResourceNotFound"; import { Studio } from "../../../components/studio"; import { DropTableConfirmationModal } from "../../../components/studio/Modal/DropTableConfirmation"; import { StudioTableActionsDropdown } from "../../../components/studio/Table/ActionsDropdown"; @@ -25,7 +25,7 @@ import type { StudioResource } from "../../../types/studio"; export const Route = createFileRoute("/do/$className/$objectId")({ component: ObjectView, - errorComponent: RouteError, + errorComponent: ResourceNotFound, loader: async ({ params }) => { // Resolve className to a namespace ID const response = await durableObjectsNamespaceListNamespaces(); diff --git a/packages/local-explorer-ui/src/routes/do/$className/index.tsx b/packages/local-explorer-ui/src/routes/do/$className/index.tsx index 594f71ea26..a4055e0167 100644 --- a/packages/local-explorer-ui/src/routes/do/$className/index.tsx +++ b/packages/local-explorer-ui/src/routes/do/$className/index.tsx @@ -7,12 +7,12 @@ import { } from "../../../api"; import DOIcon from "../../../assets/icons/durable-objects.svg?react"; import { Breadcrumbs } from "../../../components/Breadcrumbs"; -import { RouteError } from "../../../components/ResourceNotFound"; +import { ResourceNotFound } from "../../../components/ResourceNotFound"; import type { WorkersObject } from "../../../api"; export const Route = createFileRoute("/do/$className/")({ component: NamespaceView, - errorComponent: RouteError, + errorComponent: ResourceNotFound, loader: async ({ params }) => { const response = await durableObjectsNamespaceListNamespaces(); const namespaces = response.data?.result ?? []; diff --git a/packages/local-explorer-ui/src/routes/kv/$namespaceId.tsx b/packages/local-explorer-ui/src/routes/kv/$namespaceId.tsx index e309286f55..4223a8d0e3 100644 --- a/packages/local-explorer-ui/src/routes/kv/$namespaceId.tsx +++ b/packages/local-explorer-ui/src/routes/kv/$namespaceId.tsx @@ -12,13 +12,13 @@ import KVIcon from "../../assets/icons/kv.svg?react"; import { AddKVForm } from "../../components/AddKVForm"; import { Breadcrumbs } from "../../components/Breadcrumbs"; import { KVTable } from "../../components/KVTable"; -import { RouteError } from "../../components/ResourceNotFound"; +import { ResourceNotFound } from "../../components/ResourceNotFound"; import { SearchForm } from "../../components/SearchForm"; import type { KVEntry } from "../../api"; export const Route = createFileRoute("/kv/$namespaceId")({ component: NamespaceView, - errorComponent: RouteError, + errorComponent: ResourceNotFound, loader: async ({ params }) => { const keysResponse = await workersKvNamespaceListANamespace_SKeys({ path: { namespace_id: params.namespaceId }, diff --git a/packages/local-explorer-ui/src/routes/r2/$bucketName.tsx b/packages/local-explorer-ui/src/routes/r2/$bucketName.tsx index e0badc8f44..7ac9684df2 100644 --- a/packages/local-explorer-ui/src/routes/r2/$bucketName.tsx +++ b/packages/local-explorer-ui/src/routes/r2/$bucketName.tsx @@ -1,7 +1,7 @@ import { createFileRoute, Outlet } from "@tanstack/react-router"; -import { RouteError } from "../../components/ResourceNotFound"; +import { ResourceNotFound } from "../../components/ResourceNotFound"; export const Route = createFileRoute("/r2/$bucketName")({ component: () => , - errorComponent: RouteError, + errorComponent: ResourceNotFound, }); diff --git a/packages/local-explorer-ui/src/routes/r2/$bucketName/index.tsx b/packages/local-explorer-ui/src/routes/r2/$bucketName/index.tsx index 7733b94e70..039e9e63b3 100644 --- a/packages/local-explorer-ui/src/routes/r2/$bucketName/index.tsx +++ b/packages/local-explorer-ui/src/routes/r2/$bucketName/index.tsx @@ -24,7 +24,7 @@ import R2Icon from "../../../assets/icons/r2.svg?react"; import { Breadcrumbs } from "../../../components/Breadcrumbs"; import { R2ObjectTable } from "../../../components/R2ObjectTable"; import { R2UploadDialog } from "../../../components/R2UploadDialog"; -import { RouteError } from "../../../components/ResourceNotFound"; +import { ResourceNotFound } from "../../../components/ResourceNotFound"; import { withMinimumDelay } from "../../../utils/async"; import type { R2Object } from "../../../api"; @@ -35,7 +35,7 @@ export interface R2BucketSearch { export const Route = createFileRoute("/r2/$bucketName/")({ component: BucketView, - errorComponent: RouteError, + errorComponent: ResourceNotFound, loaderDeps: ({ search }) => ({ prefix: search.prefix, delimiter: search.delimiter, diff --git a/packages/local-explorer-ui/src/routes/r2/$bucketName/object.$.tsx b/packages/local-explorer-ui/src/routes/r2/$bucketName/object.$.tsx index e82339b8a0..e645963744 100644 --- a/packages/local-explorer-ui/src/routes/r2/$bucketName/object.$.tsx +++ b/packages/local-explorer-ui/src/routes/r2/$bucketName/object.$.tsx @@ -6,13 +6,13 @@ import { r2BucketDeleteObjects, r2BucketGetObject } from "../../../api"; import R2Icon from "../../../assets/icons/r2.svg?react"; import { Breadcrumbs } from "../../../components/Breadcrumbs"; import { CopyButton } from "../../../components/CopyButton"; -import { RouteError } from "../../../components/ResourceNotFound"; +import { ResourceNotFound } from "../../../components/ResourceNotFound"; import { formatDate, formatSize } from "../../../utils/format"; import type { R2HeadObjectResult } from "../../../api"; export const Route = createFileRoute("/r2/$bucketName/object/$")({ component: ObjectDetailView, - errorComponent: RouteError, + errorComponent: ResourceNotFound, loader: async ({ params }) => { const objectKey = params._splat; if (!objectKey) { diff --git a/packages/local-explorer-ui/src/routes/workflows/$workflowName.tsx b/packages/local-explorer-ui/src/routes/workflows/$workflowName.tsx index d12ac1cdbf..a8a01fc691 100644 --- a/packages/local-explorer-ui/src/routes/workflows/$workflowName.tsx +++ b/packages/local-explorer-ui/src/routes/workflows/$workflowName.tsx @@ -1,9 +1,9 @@ import { createFileRoute, Outlet } from "@tanstack/react-router"; -import { RouteError } from "../../components/ResourceNotFound"; +import { ResourceNotFound } from "../../components/ResourceNotFound"; export const Route = createFileRoute("/workflows/$workflowName")({ component: () => , - errorComponent: RouteError, + errorComponent: ResourceNotFound, loader: async ({ params }) => { return { workflowName: params.workflowName, diff --git a/packages/local-explorer-ui/src/routes/workflows/$workflowName/$instanceId.tsx b/packages/local-explorer-ui/src/routes/workflows/$workflowName/$instanceId.tsx index 3f2327c07f..bdf9d3ffd4 100644 --- a/packages/local-explorer-ui/src/routes/workflows/$workflowName/$instanceId.tsx +++ b/packages/local-explorer-ui/src/routes/workflows/$workflowName/$instanceId.tsx @@ -30,7 +30,7 @@ import { } from "../../../api"; import WorkflowsIcon from "../../../assets/icons/workflows.svg?react"; import { Breadcrumbs } from "../../../components/Breadcrumbs"; -import { RouteError } from "../../../components/ResourceNotFound"; +import { ResourceNotFound } from "../../../components/ResourceNotFound"; import { CopyButton } from "../../../components/workflows/CopyButton"; import { formatDuration, @@ -51,7 +51,7 @@ import type { export const Route = createFileRoute("/workflows/$workflowName/$instanceId")({ component: InstanceDetailView, - errorComponent: RouteError, + errorComponent: ResourceNotFound, loader: async ({ params }) => { const response = await workflowsGetInstanceDetails({ path: { diff --git a/packages/local-explorer-ui/src/routes/workflows/$workflowName/index.tsx b/packages/local-explorer-ui/src/routes/workflows/$workflowName/index.tsx index d712a3348e..9d7031d3a8 100644 --- a/packages/local-explorer-ui/src/routes/workflows/$workflowName/index.tsx +++ b/packages/local-explorer-ui/src/routes/workflows/$workflowName/index.tsx @@ -32,7 +32,7 @@ import { } from "../../../api"; import WorkflowsIcon from "../../../assets/icons/workflows.svg?react"; import { Breadcrumbs } from "../../../components/Breadcrumbs"; -import { RouteError } from "../../../components/ResourceNotFound"; +import { ResourceNotFound } from "../../../components/ResourceNotFound"; import { CreateWorkflowInstanceDialog } from "../../../components/workflows/CreateInstanceDialog"; import { timeAgo } from "../../../components/workflows/helpers"; import { WorkflowStatusBadge } from "../../../components/workflows/StatusBadge"; @@ -42,7 +42,7 @@ import type { Action } from "../../../components/workflows/types"; export const Route = createFileRoute("/workflows/$workflowName/")({ component: WorkflowInstancesView, - errorComponent: RouteError, + errorComponent: ResourceNotFound, loader: async ({ params }) => { const response = await workflowsListInstances({ path: { workflow_name: params.workflowName }, From f485ad0c9f5a35267b9fe4294aaec5525096c226 Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Thu, 2 Apr 2026 09:44:56 +0100 Subject: [PATCH 05/15] Moved not found component to shared file --- .../src/components/NotFound.tsx | 19 +++++++++++++++++++ .../local-explorer-ui/src/routes/__root.tsx | 16 ++-------------- 2 files changed, 21 insertions(+), 14 deletions(-) create mode 100644 packages/local-explorer-ui/src/components/NotFound.tsx diff --git a/packages/local-explorer-ui/src/components/NotFound.tsx b/packages/local-explorer-ui/src/components/NotFound.tsx new file mode 100644 index 0000000000..db518145ed --- /dev/null +++ b/packages/local-explorer-ui/src/components/NotFound.tsx @@ -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 ( +
+

Page not found

+ +

+ The resource you're looking for doesn't exist or has been + removed. +

+ + + + +
+ ); +} diff --git a/packages/local-explorer-ui/src/routes/__root.tsx b/packages/local-explorer-ui/src/routes/__root.tsx index ac548900f9..34a97515c2 100644 --- a/packages/local-explorer-ui/src/routes/__root.tsx +++ b/packages/local-explorer-ui/src/routes/__root.tsx @@ -15,6 +15,7 @@ import { workersKvNamespaceListNamespaces, workflowsListWorkflows, } from "../api"; +import { NotFound } from "../components/NotFound"; import { Sidebar } from "../components/Sidebar"; import { filterVisibleWorkers } from "../components/WorkerSelector"; import type { @@ -34,20 +35,7 @@ type R2BucketWithWorker = R2Bucket & { workerName?: string }; export const Route = createRootRoute({ component: RootLayout, - notFoundComponent: () => ( -
-

Page not found

- -

- The resource you're looking for doesn't exist or has been - removed. -

- - - - -
- ), + notFoundComponent: NotFound, loader: async () => { const [ workersResponse, From 775a7eaccc5e48735d2ec2ecbffae0d701e98212 Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Thu, 2 Apr 2026 16:04:59 +0100 Subject: [PATCH 06/15] Removed unused imports from app root --- packages/local-explorer-ui/src/routes/__root.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/local-explorer-ui/src/routes/__root.tsx b/packages/local-explorer-ui/src/routes/__root.tsx index 34a97515c2..3037b0f7c5 100644 --- a/packages/local-explorer-ui/src/routes/__root.tsx +++ b/packages/local-explorer-ui/src/routes/__root.tsx @@ -1,7 +1,6 @@ -import { Button, Toasty } from "@cloudflare/kumo"; +import { Toasty } from "@cloudflare/kumo"; import { createRootRoute, - Link, Outlet, useRouter, useRouterState, From ec4ccfc61509087514f44a76bead49a7588e05d5 Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Thu, 2 Apr 2026 17:51:55 +0100 Subject: [PATCH 07/15] Improved resource error message handling --- .../src/components/ResourceNotFound.tsx | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/local-explorer-ui/src/components/ResourceNotFound.tsx b/packages/local-explorer-ui/src/components/ResourceNotFound.tsx index cb66e100d8..304bf27d3c 100644 --- a/packages/local-explorer-ui/src/components/ResourceNotFound.tsx +++ b/packages/local-explorer-ui/src/components/ResourceNotFound.tsx @@ -1,14 +1,23 @@ 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 ResourceNotFound({ + error, +}: ErrorComponentProps): JSX.Element { + const details = error.errors?.[0]?.message ?? DEFAULT_ERROR_DESCRIPTION; -export function ResourceNotFound(_props: ErrorComponentProps): JSX.Element { return (
-

Resource not found

+ + +

Something went wrong

-

- This binding doesn't exist in your current dev session. -

+

{details}

From 9de75b02862893dde4b73c5b0b410c30ad6833a3 Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Thu, 2 Apr 2026 17:52:08 +0100 Subject: [PATCH 08/15] Minor `ResourceNotFound` fixes --- .../local-explorer-ui/src/components/ResourceNotFound.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/local-explorer-ui/src/components/ResourceNotFound.tsx b/packages/local-explorer-ui/src/components/ResourceNotFound.tsx index 304bf27d3c..923dcf2d34 100644 --- a/packages/local-explorer-ui/src/components/ResourceNotFound.tsx +++ b/packages/local-explorer-ui/src/components/ResourceNotFound.tsx @@ -8,8 +8,11 @@ const DEFAULT_ERROR_DESCRIPTION = export function ResourceNotFound({ error, -}: ErrorComponentProps): JSX.Element { - const details = error.errors?.[0]?.message ?? DEFAULT_ERROR_DESCRIPTION; +}: ErrorComponentProps): JSX.Element { + const details = + ("errors" in error + ? error.errors?.[0]?.message + : DEFAULT_ERROR_DESCRIPTION) ?? DEFAULT_ERROR_DESCRIPTION; return (
From f939b3d614d4bfa77c196de5ab4940f75c70835a Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Thu, 2 Apr 2026 18:37:25 +0100 Subject: [PATCH 09/15] Overhauled id routes data fetching to correctly redirect not found routes --- .../src/routes/do/$className.tsx | 4 ++-- .../src/routes/do/$className/$objectId.tsx | 5 ++++- .../src/routes/do/$className/index.tsx | 6 ++++-- .../src/routes/r2/$bucketName/index.tsx | 19 ++++++++++++++++- .../src/routes/r2/$bucketName/object.$.tsx | 19 +++++++++++++++-- .../workflows/$workflowName/$instanceId.tsx | 21 +++++++++++++++++-- .../routes/workflows/$workflowName/index.tsx | 14 ++++++++++++- 7 files changed, 77 insertions(+), 11 deletions(-) diff --git a/packages/local-explorer-ui/src/routes/do/$className.tsx b/packages/local-explorer-ui/src/routes/do/$className.tsx index c3b2b71f29..d2d55e778f 100644 --- a/packages/local-explorer-ui/src/routes/do/$className.tsx +++ b/packages/local-explorer-ui/src/routes/do/$className.tsx @@ -1,4 +1,4 @@ -import { createFileRoute, Outlet } from "@tanstack/react-router"; +import { createFileRoute, notFound, Outlet } from "@tanstack/react-router"; import { durableObjectsNamespaceListNamespaces } from "../../api"; import { ResourceNotFound } from "../../components/ResourceNotFound"; @@ -17,7 +17,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(); } return { diff --git a/packages/local-explorer-ui/src/routes/do/$className/$objectId.tsx b/packages/local-explorer-ui/src/routes/do/$className/$objectId.tsx index 9e713b7aab..a7a2d7c3ee 100644 --- a/packages/local-explorer-ui/src/routes/do/$className/$objectId.tsx +++ b/packages/local-explorer-ui/src/routes/do/$className/$objectId.tsx @@ -7,6 +7,7 @@ import { import { createFileRoute, Link, + notFound, useNavigate, useRouter, } from "@tanstack/react-router"; @@ -14,6 +15,7 @@ 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 { ResourceNotFound } from "../../../components/ResourceNotFound"; import { Studio } from "../../../components/studio"; import { DropTableConfirmationModal } from "../../../components/studio/Modal/DropTableConfirmation"; @@ -37,7 +39,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(); } // Fetch tables using the resolved namespace ID @@ -54,6 +56,7 @@ export const Route = createFileRoute("/do/$className/$objectId")({ tables, }; }, + notFoundComponent: NotFound, validateSearch: (search) => ({ table: typeof search.table === "string" ? search.table : undefined, }), diff --git a/packages/local-explorer-ui/src/routes/do/$className/index.tsx b/packages/local-explorer-ui/src/routes/do/$className/index.tsx index a4055e0167..2ff9be9c75 100644 --- a/packages/local-explorer-ui/src/routes/do/$className/index.tsx +++ b/packages/local-explorer-ui/src/routes/do/$className/index.tsx @@ -1,5 +1,5 @@ import { Button, Link as KumoLink, Table } from "@cloudflare/kumo"; -import { createFileRoute, Link } from "@tanstack/react-router"; +import { createFileRoute, Link, notFound } from "@tanstack/react-router"; import { useCallback, useEffect, useState } from "react"; import { durableObjectsNamespaceListNamespaces, @@ -7,6 +7,7 @@ import { } from "../../../api"; import DOIcon from "../../../assets/icons/durable-objects.svg?react"; import { Breadcrumbs } from "../../../components/Breadcrumbs"; +import { NotFound } from "../../../components/NotFound"; import { ResourceNotFound } from "../../../components/ResourceNotFound"; import type { WorkersObject } from "../../../api"; @@ -23,7 +24,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({ @@ -45,6 +46,7 @@ export const Route = createFileRoute("/do/$className/")({ objects, }; }, + notFoundComponent: NotFound, }); function NamespaceView() { diff --git a/packages/local-explorer-ui/src/routes/r2/$bucketName/index.tsx b/packages/local-explorer-ui/src/routes/r2/$bucketName/index.tsx index 039e9e63b3..56b4177e7d 100644 --- a/packages/local-explorer-ui/src/routes/r2/$bucketName/index.tsx +++ b/packages/local-explorer-ui/src/routes/r2/$bucketName/index.tsx @@ -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, @@ -22,6 +27,7 @@ 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 { ResourceNotFound } from "../../../components/ResourceNotFound"; @@ -50,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 ?? [], @@ -60,6 +76,7 @@ export const Route = createFileRoute("/r2/$bucketName/")({ delimiterEnabled: deps.delimiter !== false, }; }, + notFoundComponent: NotFound, validateSearch: (search: Record): R2BucketSearch => ({ prefix: typeof search.prefix === "string" ? search.prefix : undefined, delimiter: search.delimiter === false ? false : true, diff --git a/packages/local-explorer-ui/src/routes/r2/$bucketName/object.$.tsx b/packages/local-explorer-ui/src/routes/r2/$bucketName/object.$.tsx index e645963744..b8cf885a4a 100644 --- a/packages/local-explorer-ui/src/routes/r2/$bucketName/object.$.tsx +++ b/packages/local-explorer-ui/src/routes/r2/$bucketName/object.$.tsx @@ -1,11 +1,17 @@ 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 { ResourceNotFound } from "../../../components/ResourceNotFound"; import { formatDate, formatSize } from "../../../utils/format"; import type { R2HeadObjectResult } from "../../../api"; @@ -27,11 +33,19 @@ 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 { @@ -39,6 +53,7 @@ export const Route = createFileRoute("/r2/$bucketName/object/$")({ objectKey, }; }, + notFoundComponent: NotFound, }); interface ObjectDetailsCardProps { diff --git a/packages/local-explorer-ui/src/routes/workflows/$workflowName/$instanceId.tsx b/packages/local-explorer-ui/src/routes/workflows/$workflowName/$instanceId.tsx index bdf9d3ffd4..fb06989552 100644 --- a/packages/local-explorer-ui/src/routes/workflows/$workflowName/$instanceId.tsx +++ b/packages/local-explorer-ui/src/routes/workflows/$workflowName/$instanceId.tsx @@ -20,7 +20,12 @@ import { StopIcon, TrashIcon, } from "@phosphor-icons/react"; -import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; +import { + createFileRoute, + Link, + notFound, + useNavigate, +} from "@tanstack/react-router"; import { memo, useCallback, useEffect, useRef, useState } from "react"; import { workflowsChangeInstanceStatus, @@ -30,6 +35,7 @@ import { } from "../../../api"; import WorkflowsIcon from "../../../assets/icons/workflows.svg?react"; import { Breadcrumbs } from "../../../components/Breadcrumbs"; +import { NotFound } from "../../../components/NotFound"; import { ResourceNotFound } from "../../../components/ResourceNotFound"; import { CopyButton } from "../../../components/workflows/CopyButton"; import { @@ -58,15 +64,26 @@ export const Route = createFileRoute("/workflows/$workflowName/$instanceId")({ instance_id: params.instanceId, workflow_name: params.workflowName, }, + throwOnError: false, }); + if (response.response?.status === 404) { + throw notFound(); + } + + if (response.error) { + throw new Error( + `Failed to fetch workflow instance "${params.instanceId}"` + ); + } const details = response.data?.result as InstanceDetails | undefined; if (!details) { - throw new Error(`Workflow instance "${params.instanceId}" not found.`); + throw notFound(); } return { details }; }, + notFoundComponent: NotFound, }); // --------------------------------------------------------------------------- diff --git a/packages/local-explorer-ui/src/routes/workflows/$workflowName/index.tsx b/packages/local-explorer-ui/src/routes/workflows/$workflowName/index.tsx index 9d7031d3a8..cc35255999 100644 --- a/packages/local-explorer-ui/src/routes/workflows/$workflowName/index.tsx +++ b/packages/local-explorer-ui/src/routes/workflows/$workflowName/index.tsx @@ -21,7 +21,7 @@ import { StopIcon, TrashIcon, } from "@phosphor-icons/react"; -import { createFileRoute, useNavigate } from "@tanstack/react-router"; +import { createFileRoute, notFound, useNavigate } from "@tanstack/react-router"; import { memo, useCallback, useEffect, useRef, useState } from "react"; import { workflowsChangeInstanceStatus, @@ -32,6 +32,7 @@ import { } from "../../../api"; import WorkflowsIcon from "../../../assets/icons/workflows.svg?react"; import { Breadcrumbs } from "../../../components/Breadcrumbs"; +import { NotFound } from "../../../components/NotFound"; import { ResourceNotFound } from "../../../components/ResourceNotFound"; import { CreateWorkflowInstanceDialog } from "../../../components/workflows/CreateInstanceDialog"; import { timeAgo } from "../../../components/workflows/helpers"; @@ -47,7 +48,17 @@ export const Route = createFileRoute("/workflows/$workflowName/")({ const response = await workflowsListInstances({ path: { workflow_name: params.workflowName }, query: { page: 1, per_page: 25 }, + throwOnError: false, }); + if (response.response?.status === 404) { + throw notFound(); + } + + if (response.error) { + throw new Error( + `Failed to list instances for workflow "${params.workflowName}"` + ); + } return { instances: response.data?.result ?? [], @@ -59,6 +70,7 @@ export const Route = createFileRoute("/workflows/$workflowName/")({ }, }; }, + notFoundComponent: NotFound, }); // --------------------------------------------------------------------------- From 8b1a2294a96e4c9063c3af56c2248a74db54cb1b Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Thu, 2 Apr 2026 18:41:10 +0100 Subject: [PATCH 10/15] Renamed `ResourceNotFound` to `ResourceError` --- .../components/{ResourceNotFound.tsx => ResourceError.tsx} | 2 +- packages/local-explorer-ui/src/routes/d1/$databaseId.tsx | 4 ++-- packages/local-explorer-ui/src/routes/do/$className.tsx | 4 ++-- .../local-explorer-ui/src/routes/do/$className/$objectId.tsx | 4 ++-- packages/local-explorer-ui/src/routes/do/$className/index.tsx | 4 ++-- packages/local-explorer-ui/src/routes/kv/$namespaceId.tsx | 4 ++-- packages/local-explorer-ui/src/routes/r2/$bucketName.tsx | 4 ++-- .../local-explorer-ui/src/routes/r2/$bucketName/index.tsx | 4 ++-- .../local-explorer-ui/src/routes/r2/$bucketName/object.$.tsx | 4 ++-- .../local-explorer-ui/src/routes/workflows/$workflowName.tsx | 4 ++-- .../src/routes/workflows/$workflowName/$instanceId.tsx | 4 ++-- .../src/routes/workflows/$workflowName/index.tsx | 4 ++-- 12 files changed, 23 insertions(+), 23 deletions(-) rename packages/local-explorer-ui/src/components/{ResourceNotFound.tsx => ResourceError.tsx} (96%) diff --git a/packages/local-explorer-ui/src/components/ResourceNotFound.tsx b/packages/local-explorer-ui/src/components/ResourceError.tsx similarity index 96% rename from packages/local-explorer-ui/src/components/ResourceNotFound.tsx rename to packages/local-explorer-ui/src/components/ResourceError.tsx index 923dcf2d34..d738afa8df 100644 --- a/packages/local-explorer-ui/src/components/ResourceNotFound.tsx +++ b/packages/local-explorer-ui/src/components/ResourceError.tsx @@ -6,7 +6,7 @@ import type { WorkersApiResponseCommonFailure } from "../api"; const DEFAULT_ERROR_DESCRIPTION = "An unknown error occured. Please report this issue to Cloudflare."; -export function ResourceNotFound({ +export function ResourceError({ error, }: ErrorComponentProps): JSX.Element { const details = diff --git a/packages/local-explorer-ui/src/routes/d1/$databaseId.tsx b/packages/local-explorer-ui/src/routes/d1/$databaseId.tsx index 76b9aef007..77a61b7457 100644 --- a/packages/local-explorer-ui/src/routes/d1/$databaseId.tsx +++ b/packages/local-explorer-ui/src/routes/d1/$databaseId.tsx @@ -13,7 +13,7 @@ import { import { useCallback, useMemo, useRef, useState } from "react"; import D1Icon from "../../assets/icons/d1.svg?react"; import { Breadcrumbs } from "../../components/Breadcrumbs"; -import { ResourceNotFound } from "../../components/ResourceNotFound"; +import { ResourceError } from "../../components/ResourceError"; import { Studio } from "../../components/studio"; import { DropTableConfirmationModal } from "../../components/studio/Modal/DropTableConfirmation"; import { StudioTableActionsDropdown } from "../../components/studio/Table/ActionsDropdown"; @@ -24,7 +24,7 @@ import type { StudioResource } from "../../types/studio"; export const Route = createFileRoute("/d1/$databaseId")({ component: DatabaseView, - errorComponent: ResourceNotFound, + errorComponent: ResourceError, loader: async (ctx) => { const driver = new LocalD1Driver(ctx.params.databaseId); const schemas = await driver.schemas(); diff --git a/packages/local-explorer-ui/src/routes/do/$className.tsx b/packages/local-explorer-ui/src/routes/do/$className.tsx index d2d55e778f..699aaa6ef3 100644 --- a/packages/local-explorer-ui/src/routes/do/$className.tsx +++ b/packages/local-explorer-ui/src/routes/do/$className.tsx @@ -1,10 +1,10 @@ import { createFileRoute, notFound, Outlet } from "@tanstack/react-router"; import { durableObjectsNamespaceListNamespaces } from "../../api"; -import { ResourceNotFound } from "../../components/ResourceNotFound"; +import { ResourceError } from "../../components/ResourceError"; export const Route = createFileRoute("/do/$className")({ component: () => , - errorComponent: ResourceNotFound, + errorComponent: ResourceError, loader: async ({ params }) => { const response = await durableObjectsNamespaceListNamespaces(); const namespaces = response.data?.result ?? []; diff --git a/packages/local-explorer-ui/src/routes/do/$className/$objectId.tsx b/packages/local-explorer-ui/src/routes/do/$className/$objectId.tsx index a7a2d7c3ee..eaf53c965f 100644 --- a/packages/local-explorer-ui/src/routes/do/$className/$objectId.tsx +++ b/packages/local-explorer-ui/src/routes/do/$className/$objectId.tsx @@ -16,7 +16,7 @@ import { durableObjectsNamespaceListNamespaces } from "../../../api"; import DOIcon from "../../../assets/icons/durable-objects.svg?react"; import { Breadcrumbs } from "../../../components/Breadcrumbs"; import { NotFound } from "../../../components/NotFound"; -import { ResourceNotFound } from "../../../components/ResourceNotFound"; +import { ResourceError } from "../../../components/ResourceError"; import { Studio } from "../../../components/studio"; import { DropTableConfirmationModal } from "../../../components/studio/Modal/DropTableConfirmation"; import { StudioTableActionsDropdown } from "../../../components/studio/Table/ActionsDropdown"; @@ -27,7 +27,7 @@ import type { StudioResource } from "../../../types/studio"; export const Route = createFileRoute("/do/$className/$objectId")({ component: ObjectView, - errorComponent: ResourceNotFound, + errorComponent: ResourceError, loader: async ({ params }) => { // Resolve className to a namespace ID const response = await durableObjectsNamespaceListNamespaces(); diff --git a/packages/local-explorer-ui/src/routes/do/$className/index.tsx b/packages/local-explorer-ui/src/routes/do/$className/index.tsx index 2ff9be9c75..dbde8e5b22 100644 --- a/packages/local-explorer-ui/src/routes/do/$className/index.tsx +++ b/packages/local-explorer-ui/src/routes/do/$className/index.tsx @@ -8,12 +8,12 @@ import { import DOIcon from "../../../assets/icons/durable-objects.svg?react"; import { Breadcrumbs } from "../../../components/Breadcrumbs"; import { NotFound } from "../../../components/NotFound"; -import { ResourceNotFound } from "../../../components/ResourceNotFound"; +import { ResourceError } from "../../../components/ResourceError"; import type { WorkersObject } from "../../../api"; export const Route = createFileRoute("/do/$className/")({ component: NamespaceView, - errorComponent: ResourceNotFound, + errorComponent: ResourceError, loader: async ({ params }) => { const response = await durableObjectsNamespaceListNamespaces(); const namespaces = response.data?.result ?? []; diff --git a/packages/local-explorer-ui/src/routes/kv/$namespaceId.tsx b/packages/local-explorer-ui/src/routes/kv/$namespaceId.tsx index 4223a8d0e3..2456b63b59 100644 --- a/packages/local-explorer-ui/src/routes/kv/$namespaceId.tsx +++ b/packages/local-explorer-ui/src/routes/kv/$namespaceId.tsx @@ -12,13 +12,13 @@ import KVIcon from "../../assets/icons/kv.svg?react"; import { AddKVForm } from "../../components/AddKVForm"; import { Breadcrumbs } from "../../components/Breadcrumbs"; import { KVTable } from "../../components/KVTable"; -import { ResourceNotFound } from "../../components/ResourceNotFound"; +import { ResourceError } from "../../components/ResourceError"; import { SearchForm } from "../../components/SearchForm"; import type { KVEntry } from "../../api"; export const Route = createFileRoute("/kv/$namespaceId")({ component: NamespaceView, - errorComponent: ResourceNotFound, + errorComponent: ResourceError, loader: async ({ params }) => { const keysResponse = await workersKvNamespaceListANamespace_SKeys({ path: { namespace_id: params.namespaceId }, diff --git a/packages/local-explorer-ui/src/routes/r2/$bucketName.tsx b/packages/local-explorer-ui/src/routes/r2/$bucketName.tsx index 7ac9684df2..561a18a08c 100644 --- a/packages/local-explorer-ui/src/routes/r2/$bucketName.tsx +++ b/packages/local-explorer-ui/src/routes/r2/$bucketName.tsx @@ -1,7 +1,7 @@ import { createFileRoute, Outlet } from "@tanstack/react-router"; -import { ResourceNotFound } from "../../components/ResourceNotFound"; +import { ResourceError } from "../../components/ResourceError"; export const Route = createFileRoute("/r2/$bucketName")({ component: () => , - errorComponent: ResourceNotFound, + errorComponent: ResourceError, }); diff --git a/packages/local-explorer-ui/src/routes/r2/$bucketName/index.tsx b/packages/local-explorer-ui/src/routes/r2/$bucketName/index.tsx index 56b4177e7d..0889557e5d 100644 --- a/packages/local-explorer-ui/src/routes/r2/$bucketName/index.tsx +++ b/packages/local-explorer-ui/src/routes/r2/$bucketName/index.tsx @@ -30,7 +30,7 @@ import { Breadcrumbs } from "../../../components/Breadcrumbs"; import { NotFound } from "../../../components/NotFound"; import { R2ObjectTable } from "../../../components/R2ObjectTable"; import { R2UploadDialog } from "../../../components/R2UploadDialog"; -import { ResourceNotFound } from "../../../components/ResourceNotFound"; +import { ResourceError } from "../../../components/ResourceError"; import { withMinimumDelay } from "../../../utils/async"; import type { R2Object } from "../../../api"; @@ -41,7 +41,7 @@ export interface R2BucketSearch { export const Route = createFileRoute("/r2/$bucketName/")({ component: BucketView, - errorComponent: ResourceNotFound, + errorComponent: ResourceError, loaderDeps: ({ search }) => ({ prefix: search.prefix, delimiter: search.delimiter, diff --git a/packages/local-explorer-ui/src/routes/r2/$bucketName/object.$.tsx b/packages/local-explorer-ui/src/routes/r2/$bucketName/object.$.tsx index b8cf885a4a..707df59c45 100644 --- a/packages/local-explorer-ui/src/routes/r2/$bucketName/object.$.tsx +++ b/packages/local-explorer-ui/src/routes/r2/$bucketName/object.$.tsx @@ -12,13 +12,13 @@ import R2Icon from "../../../assets/icons/r2.svg?react"; import { Breadcrumbs } from "../../../components/Breadcrumbs"; import { CopyButton } from "../../../components/CopyButton"; import { NotFound } from "../../../components/NotFound"; -import { ResourceNotFound } from "../../../components/ResourceNotFound"; +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: ResourceNotFound, + errorComponent: ResourceError, loader: async ({ params }) => { const objectKey = params._splat; if (!objectKey) { diff --git a/packages/local-explorer-ui/src/routes/workflows/$workflowName.tsx b/packages/local-explorer-ui/src/routes/workflows/$workflowName.tsx index a8a01fc691..165a38aa93 100644 --- a/packages/local-explorer-ui/src/routes/workflows/$workflowName.tsx +++ b/packages/local-explorer-ui/src/routes/workflows/$workflowName.tsx @@ -1,9 +1,9 @@ import { createFileRoute, Outlet } from "@tanstack/react-router"; -import { ResourceNotFound } from "../../components/ResourceNotFound"; +import { ResourceError } from "../../components/ResourceError"; export const Route = createFileRoute("/workflows/$workflowName")({ component: () => , - errorComponent: ResourceNotFound, + errorComponent: ResourceError, loader: async ({ params }) => { return { workflowName: params.workflowName, diff --git a/packages/local-explorer-ui/src/routes/workflows/$workflowName/$instanceId.tsx b/packages/local-explorer-ui/src/routes/workflows/$workflowName/$instanceId.tsx index fb06989552..203afc4be7 100644 --- a/packages/local-explorer-ui/src/routes/workflows/$workflowName/$instanceId.tsx +++ b/packages/local-explorer-ui/src/routes/workflows/$workflowName/$instanceId.tsx @@ -36,7 +36,7 @@ import { import WorkflowsIcon from "../../../assets/icons/workflows.svg?react"; import { Breadcrumbs } from "../../../components/Breadcrumbs"; import { NotFound } from "../../../components/NotFound"; -import { ResourceNotFound } from "../../../components/ResourceNotFound"; +import { ResourceError } from "../../../components/ResourceError"; import { CopyButton } from "../../../components/workflows/CopyButton"; import { formatDuration, @@ -57,7 +57,7 @@ import type { export const Route = createFileRoute("/workflows/$workflowName/$instanceId")({ component: InstanceDetailView, - errorComponent: ResourceNotFound, + errorComponent: ResourceError, loader: async ({ params }) => { const response = await workflowsGetInstanceDetails({ path: { diff --git a/packages/local-explorer-ui/src/routes/workflows/$workflowName/index.tsx b/packages/local-explorer-ui/src/routes/workflows/$workflowName/index.tsx index cc35255999..c62d4505b5 100644 --- a/packages/local-explorer-ui/src/routes/workflows/$workflowName/index.tsx +++ b/packages/local-explorer-ui/src/routes/workflows/$workflowName/index.tsx @@ -33,7 +33,7 @@ import { import WorkflowsIcon from "../../../assets/icons/workflows.svg?react"; import { Breadcrumbs } from "../../../components/Breadcrumbs"; import { NotFound } from "../../../components/NotFound"; -import { ResourceNotFound } from "../../../components/ResourceNotFound"; +import { ResourceError } from "../../../components/ResourceError"; import { CreateWorkflowInstanceDialog } from "../../../components/workflows/CreateInstanceDialog"; import { timeAgo } from "../../../components/workflows/helpers"; import { WorkflowStatusBadge } from "../../../components/workflows/StatusBadge"; @@ -43,7 +43,7 @@ import type { Action } from "../../../components/workflows/types"; export const Route = createFileRoute("/workflows/$workflowName/")({ component: WorkflowInstancesView, - errorComponent: ResourceNotFound, + errorComponent: ResourceError, loader: async ({ params }) => { const response = await workflowsListInstances({ path: { workflow_name: params.workflowName }, From ccd365f6825c264bcc5cac1ba501ada25a452af1 Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Thu, 2 Apr 2026 18:55:10 +0100 Subject: [PATCH 11/15] Updated changeset description --- .changeset/happy-cameras-occur.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/happy-cameras-occur.md b/.changeset/happy-cameras-occur.md index 6b041e28ea..19f0adc59e 100644 --- a/.changeset/happy-cameras-occur.md +++ b/.changeset/happy-cameras-occur.md @@ -2,8 +2,8 @@ "@cloudflare/local-explorer-ui": patch --- -Fix local explorer invalid route messages. +Improves local explorer invalid route error handling. -When trying to access a route, like `/foo`, that doesn't exist we show a custom 404 error message with a redirect button. +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. -Similarly we do the same for when trying to access a binding resource that doesn't exist. Now showing a "Resource not found" error with a redirect button. +Additionally, it also fixes route loaders to correctly throw a 404 error if a resource is not found, rather than showing a generic error. From d6f0a10e4cbef649c50703b40942964d8775aaba Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Thu, 2 Apr 2026 18:57:45 +0100 Subject: [PATCH 12/15] Fixed DO $className not found handling --- packages/local-explorer-ui/src/routes/do/$className.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/local-explorer-ui/src/routes/do/$className.tsx b/packages/local-explorer-ui/src/routes/do/$className.tsx index 699aaa6ef3..914b2b4cbf 100644 --- a/packages/local-explorer-ui/src/routes/do/$className.tsx +++ b/packages/local-explorer-ui/src/routes/do/$className.tsx @@ -1,5 +1,6 @@ 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")({ @@ -25,4 +26,5 @@ export const Route = createFileRoute("/do/$className")({ namespaceId: namespace.id, }; }, + notFoundComponent: NotFound, }); From 3316ac15bd05a3e13eff9f2fe44eead9361b31c2 Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Wed, 8 Apr 2026 09:56:33 +0100 Subject: [PATCH 13/15] Fixed error details mapping --- packages/local-explorer-ui/src/components/ResourceError.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/local-explorer-ui/src/components/ResourceError.tsx b/packages/local-explorer-ui/src/components/ResourceError.tsx index d738afa8df..f422ce1bfa 100644 --- a/packages/local-explorer-ui/src/components/ResourceError.tsx +++ b/packages/local-explorer-ui/src/components/ResourceError.tsx @@ -10,9 +10,8 @@ export function ResourceError({ error, }: ErrorComponentProps): JSX.Element { const details = - ("errors" in error - ? error.errors?.[0]?.message - : DEFAULT_ERROR_DESCRIPTION) ?? DEFAULT_ERROR_DESCRIPTION; + ("errors" in error ? error.errors?.[0]?.message : error.message) ?? + DEFAULT_ERROR_DESCRIPTION; return (
From ff6b02e4637bf8873c63ff89ad3b83db8126175d Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Wed, 8 Apr 2026 09:56:39 +0100 Subject: [PATCH 14/15] Minor code formatting --- packages/local-explorer-ui/src/components/NotFound.tsx | 6 +++--- packages/local-explorer-ui/src/components/ResourceError.tsx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/local-explorer-ui/src/components/NotFound.tsx b/packages/local-explorer-ui/src/components/NotFound.tsx index db518145ed..2dea78d853 100644 --- a/packages/local-explorer-ui/src/components/NotFound.tsx +++ b/packages/local-explorer-ui/src/components/NotFound.tsx @@ -3,10 +3,10 @@ import { Link, type NotFoundRouteProps } from "@tanstack/react-router"; export function NotFound(_props: NotFoundRouteProps): JSX.Element { return ( -
-

Page not found

+
+

Page not found

-

+

The resource you're looking for doesn't exist or has been removed.

diff --git a/packages/local-explorer-ui/src/components/ResourceError.tsx b/packages/local-explorer-ui/src/components/ResourceError.tsx index f422ce1bfa..84ea8113a4 100644 --- a/packages/local-explorer-ui/src/components/ResourceError.tsx +++ b/packages/local-explorer-ui/src/components/ResourceError.tsx @@ -14,12 +14,12 @@ export function ResourceError({ DEFAULT_ERROR_DESCRIPTION; return ( -
+
-

Something went wrong

+

Something went wrong

-

{details}

+

{details}

From 0dec399f6da144ba15bf90f1e07e9689bd641cea Mon Sep 17 00:00:00 2001 From: Ben Dixon Date: Thu, 9 Apr 2026 15:14:25 +0100 Subject: [PATCH 15/15] Fixed DO table column name --- packages/local-explorer-ui/src/routes/do/$className/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/local-explorer-ui/src/routes/do/$className/index.tsx b/packages/local-explorer-ui/src/routes/do/$className/index.tsx index 16d2ce2dd2..b9909cd4ec 100644 --- a/packages/local-explorer-ui/src/routes/do/$className/index.tsx +++ b/packages/local-explorer-ui/src/routes/do/$className/index.tsx @@ -216,7 +216,7 @@ function NamespaceView() { {objects.map((obj) => ( - {obj.id ?? "—"} + {obj.name ?? "—"} {obj.id}