diff --git a/.changeset/update-local-explorer-sidebar.md b/.changeset/update-local-explorer-sidebar.md new file mode 100644 index 0000000000..8300df284a --- /dev/null +++ b/.changeset/update-local-explorer-sidebar.md @@ -0,0 +1,11 @@ +--- +"@cloudflare/local-explorer-ui": minor +--- + +Update local explorer sidebar with collapsible groups, theme persistence, and Kumo v1.17 + +Adds localStorage persistence for sidebar group expansion states and theme mode (light/dark/system). The sidebar now uses Kumo v1.17 primitives with collapsible groups and a theme toggle in the footer. + +Users can now cycle between light, dark, and system theme modes, and their preference will be persisted across sessions. + +Sidebar groups (D1, Durable Objects, KV, R2, Workflows) also remember their collapsed/expanded state. diff --git a/packages/local-explorer-ui/package.json b/packages/local-explorer-ui/package.json index 8e75059a41..d3b005bbad 100644 --- a/packages/local-explorer-ui/package.json +++ b/packages/local-explorer-ui/package.json @@ -22,7 +22,7 @@ }, "dependencies": { "@base-ui/react": "^1.1.0", - "@cloudflare/kumo": "^1.5.0", + "@cloudflare/kumo": "^1.17.0", "@cloudflare/workers-editor-shared": "^0.1.1", "@codemirror/autocomplete": "^6.20.0", "@codemirror/commands": "^6.10.2", diff --git a/packages/local-explorer-ui/src/__e2e__/r2/r2-bucket.spec.ts b/packages/local-explorer-ui/src/__e2e__/r2/r2-bucket.spec.ts index 199627238c..87a928c3d7 100644 --- a/packages/local-explorer-ui/src/__e2e__/r2/r2-bucket.spec.ts +++ b/packages/local-explorer-ui/src/__e2e__/r2/r2-bucket.spec.ts @@ -8,6 +8,7 @@ import { navigateToR2Object, page, seedR2, + waitForBreadcrumbText, waitForDialog, waitForTableRows, waitForText, @@ -88,8 +89,8 @@ describe("R2 Bucket", () => { await navigateToR2Bucket("my-bucket"); await waitForTableRows(1); - await waitForText("R2"); - await waitForText("my-bucket"); + await waitForBreadcrumbText("R2"); + await waitForBreadcrumbText("my-bucket"); }); test("navigates into a directory and updates breadcrumbs", async () => { @@ -273,10 +274,10 @@ describe("R2 Bucket", () => { await navigateToR2Object("my-bucket", "documents/report.txt"); // Breadcrumbs should show: R2 > my-bucket > documents > report.txt - await waitForText("R2"); - await waitForText("my-bucket"); - await waitForText("documents"); - await waitForText("report.txt"); + await waitForBreadcrumbText("R2"); + await waitForBreadcrumbText("my-bucket"); + await waitForBreadcrumbText("documents"); + await waitForBreadcrumbText("report.txt"); }); }); @@ -358,7 +359,11 @@ describe("R2 Bucket", () => { await waitForDialog(); await waitForText("Delete object?"); - await waitForText("readme.txt"); + + // "readme.txt" appears in multiple places (sidebar, breadcrumbs, heading), + // so scope the assertion to the dialog. + const dialog = page.getByRole("dialog"); + await dialog.getByText("readme.txt").waitFor({ state: "visible" }); }); test("deletes object from detail page and navigates back", async () => { diff --git a/packages/local-explorer-ui/src/__e2e__/utils.ts b/packages/local-explorer-ui/src/__e2e__/utils.ts index fb33535978..78b6ba8e20 100644 --- a/packages/local-explorer-ui/src/__e2e__/utils.ts +++ b/packages/local-explorer-ui/src/__e2e__/utils.ts @@ -144,27 +144,28 @@ export async function navigateToDOObject( */ export async function navigateToDOObjectByName( className: string, - table?: string + table?: string, + objectName: string = "test-object" ): Promise { await navigateToDOClass(className); await waitForText(className); - const openStudioLink = page.locator('a:has-text("Open Studio")').first(); - await openStudioLink.waitFor({ state: "visible", timeout: 10_000 }); - - const href = await openStudioLink.getAttribute("href"); - if (!href) { - throw new Error("Could not find href on Open Studio link"); - } + await fillByPlaceholder("Enter instance name or hex ID...", objectName); + await page.getByRole("button", { name: "Open Studio" }).click(); + await waitForPageLoad(); - // Extract the object ID from the href (format: /cdn-cgi/explorer/do/{className}/{objectId}) - const match = href.match(/\/cdn-cgi\/explorer\/do\/[^/]+\/([a-f0-9]+)/); + // Extract the object ID from the current URL after navigation. + const objectPath = new URL(page.url()).pathname; + const match = objectPath.match(/\/cdn-cgi\/explorer\/do\/[^/]+\/([^/?#]+)/); if (!match || !match[1]) { - throw new Error(`Could not extract object ID from href: ${href}`); + throw new Error(`Could not extract object ID from URL path: ${objectPath}`); } + const objectId: string = match[1]; - await navigateToDOObject(className, objectId, table); + if (table) { + await navigateToDOObject(className, objectId, table); + } return objectId; } @@ -183,6 +184,34 @@ export async function waitForText( }); } +/** + * Wait for text to appear inside the breadcrumb navigation bar. + * + * Use this instead of `waitForText` when the same text also appears in the + * sidebar (e.g. bucket names, object keys) to avoid Playwright resolving + * to the wrong element. + */ +export async function waitForBreadcrumbText( + text: string, + options?: { + timeout?: number; + } +): Promise { + // The Kumo breadcrumb `