-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat(local-explorer-ui): Added initial data studio plumbing #12518
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 37 commits
68487ef
7f43d5c
334acbe
43a6b47
ae30d73
c7deb47
e1526b4
d3082c0
87bb21c
c0bef86
50d44ec
93ac650
ca30e3f
fb7bbf7
22a69e6
09133d9
f3187e7
39c88ec
1e7e857
12138db
2e810b3
e26192d
355e6b9
44b98b8
0c86976
5e9789f
589b7bd
0f61c9c
31dd02b
3cd4e28
4525660
cbd2e8c
9378280
17fdca7
ac29fe9
23656ba
1bafa14
82536b7
a82a064
b516b8d
fa79a07
690d78a
bcb4087
d934801
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| --- | ||
| "@cloudflare/local-explorer-ui": minor | ||
| --- | ||
|
|
||
| Implemented initial data studio driver support. | ||
|
|
||
| This provides the initial plumbing needed to add the complete data studio component to the local explorer in a later PR. | ||
|
|
||
| This is an experimental WIP feature. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import { CaretRightIcon } from "@phosphor-icons/react"; | ||
| import type { FC } from "react"; | ||
|
|
||
| interface BreadcrumbsProps { | ||
| icon: FC; | ||
| title: string; | ||
| items: Array<string>; | ||
| } | ||
|
|
||
| export function Breadcrumbs({ | ||
| icon: Icon, | ||
| items, | ||
| title, | ||
| }: BreadcrumbsProps): JSX.Element { | ||
| return ( | ||
| <div className="flex items-center gap-2 py-4 px-6 min-h-16.75 box-border bg-bg-secondary border-b border-border text-sm shrink-0"> | ||
| <span className="flex items-center gap-1.5"> | ||
| <Icon /> | ||
| {title} | ||
| </span> | ||
|
|
||
| {items.map((item) => ( | ||
| <> | ||
| <CaretRightIcon className="w-4 h-4" /> | ||
| <span className="flex items-center gap-1.5">{item}</span> | ||
| </> | ||
| ))} | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,28 +1,114 @@ | ||
| import { Collapsible } from "@base-ui/react/collapsible"; | ||
| import { cn } from "@cloudflare/kumo"; | ||
| import { CaretRightIcon } from "@phosphor-icons/react"; | ||
| import { CaretRightIcon, DatabaseIcon } from "@phosphor-icons/react"; | ||
| import { Link } from "@tanstack/react-router"; | ||
| import CloudflareLogo from "../assets/icons/cloudflare-logo.svg?react"; | ||
| import KVIcon from "../assets/icons/kv.svg?react"; | ||
| import type { WorkersKvNamespace } from "../api"; | ||
| import type { D1DatabaseResponse, WorkersKvNamespace } from "../api"; | ||
| import type { FileRouteTypes } from "../routeTree.gen"; | ||
| import type { FC } from "react"; | ||
|
|
||
| interface SidebarProps { | ||
| namespaces: WorkersKvNamespace[]; | ||
| loading: boolean; | ||
| interface SidebarItemGroupProps { | ||
| emptyLabel: string; | ||
| error: string | null; | ||
| icon: FC<{ className?: string }>; | ||
| items: Array<{ | ||
| id: string; | ||
| isActive: boolean; | ||
| label: string; | ||
| link: { | ||
| params: object; | ||
| search?: object; | ||
| to: FileRouteTypes["to"]; | ||
| }; | ||
| }>; | ||
| loading: boolean; | ||
| title: string; | ||
| } | ||
|
|
||
| function SidebarItemGroup({ | ||
| emptyLabel, | ||
| error, | ||
| icon: Icon, | ||
| items, | ||
| loading, | ||
| title, | ||
| }: SidebarItemGroupProps): JSX.Element { | ||
| return ( | ||
| <Collapsible.Root defaultOpen> | ||
| <Collapsible.Trigger className="group flex items-center gap-2 w-full py-3 px-4 border-0 border-b border-border bg-transparent font-semibold text-[11px] uppercase tracking-wide text-text-secondary cursor-pointer transition-colors hover:bg-border"> | ||
| <CaretRightIcon className="transition-transform duration-200 group-data-panel-open:rotate-90" /> | ||
| <Icon className="w-3.5 h-3.5" /> | ||
| {title} | ||
| </Collapsible.Trigger> | ||
|
|
||
| <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 flex-1 overflow-y-auto"> | ||
| {loading ? ( | ||
| <li className="block py-2.5 px-4 text-text-secondary border-b border-border"> | ||
| Loading... | ||
| </li> | ||
| ) : null} | ||
|
|
||
| {error ? ( | ||
| <li className="block py-2.5 px-4 text-danger border-b border-border"> | ||
| {error} | ||
| </li> | ||
| ) : null} | ||
|
|
||
| {!loading && !error | ||
| ? items.map((item) => ( | ||
| <li key={item.id}> | ||
| <Link | ||
| className={cn( | ||
| "block py-2.5 px-4 text-text no-underline border-b border-border cursor-pointer transition-colors hover:bg-border", | ||
| { | ||
| "bg-primary/8 text-primary border-l-3 border-l-primary pl-3.25": | ||
| item.isActive, | ||
| } | ||
| )} | ||
| params={item.link.params} | ||
| search={item.link.search} | ||
| to={item.link.to} | ||
| > | ||
| {item.label} | ||
| </Link> | ||
| </li> | ||
| )) | ||
| : null} | ||
|
|
||
| {!loading && !error && items.length === 0 && ( | ||
| <li className="block py-2.5 px-4 text-text-secondary border-b border-border"> | ||
| {emptyLabel} | ||
| </li> | ||
| )} | ||
| </ul> | ||
| </Collapsible.Panel> | ||
| </Collapsible.Root> | ||
| ); | ||
| } | ||
|
|
||
| interface SidebarProps { | ||
| currentPath: string; | ||
| d1Error: string | null; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we have a single error banner? instead of making it resource specific. i see the pr description says you made it more granular but i'm just wondering why
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I thought about this as well. The only reason I made these errors unique is so if (for whatever reason) D1 databases fail to load but KV does, you can still access your local KV namespaces and not have the sidebar just set all items to render an error.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about setting a single error, and returning an empty array for the problematic resource if there is an error? |
||
| databases: D1DatabaseResponse[]; | ||
| kvError: string | null; | ||
| loading: boolean; | ||
| namespaces: WorkersKvNamespace[]; | ||
| } | ||
|
|
||
| export function Sidebar({ | ||
| namespaces, | ||
| loading, | ||
| error, | ||
| currentPath, | ||
| d1Error, | ||
| databases, | ||
| kvError, | ||
| loading, | ||
| namespaces, | ||
| }: SidebarProps) { | ||
| return ( | ||
| <aside className="w-sidebar bg-bg-secondary border-r border-border flex flex-col"> | ||
| <a | ||
| className="flex items-center gap-2.5 p-4 border-b border-border min-h-[67px] box-border" | ||
| className="flex items-center gap-2.5 p-4 border-b border-border min-h-16.75 box-border" | ||
NuroDev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| href="/" | ||
| > | ||
| <CloudflareLogo className="shrink-0 text-primary" /> | ||
|
|
@@ -31,58 +117,45 @@ export function Sidebar({ | |
| Local Explorer | ||
| </span> | ||
| <span className="text-[10px] font-medium text-text-secondary uppercase tracking-wide"> | ||
| Cloudflare Dev Tools | ||
| Cloudflare DevTools | ||
| </span> | ||
| </div> | ||
| </a> | ||
|
|
||
| <Collapsible.Root defaultOpen> | ||
| <Collapsible.Trigger className="group flex items-center gap-2 w-full py-3 px-4 border-0 border-b border-border bg-transparent font-semibold text-[11px] uppercase tracking-wide text-text-secondary cursor-pointer transition-colors hover:bg-border"> | ||
| <CaretRightIcon className="transition-transform duration-200 group-data-[panel-open]:rotate-90" /> | ||
| <KVIcon className="w-3.5 h-3.5" /> | ||
| KV Namespaces | ||
| </Collapsible.Trigger> | ||
| <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 flex-1 overflow-y-auto"> | ||
| {loading && ( | ||
| <li className="block py-2.5 px-4 text-text-secondary border-b border-border"> | ||
| Loading... | ||
| </li> | ||
| )} | ||
| {error && ( | ||
| <li className="block py-2.5 px-4 text-danger border-b border-border"> | ||
| {error} | ||
| </li> | ||
| )} | ||
| {!loading && | ||
| !error && | ||
| namespaces.map((ns) => { | ||
| const isActive = currentPath === `/kv/${ns.id}`; | ||
| return ( | ||
| <li key={ns.id}> | ||
| <Link | ||
| to="/kv/$namespaceId" | ||
| params={{ namespaceId: ns.id }} | ||
| className={cn( | ||
| "block py-2.5 px-4 text-text no-underline border-b border-border cursor-pointer transition-colors hover:bg-border", | ||
| isActive | ||
| ? "bg-primary/8 text-primary border-l-3 border-l-primary pl-[13px]" | ||
| : "" | ||
| )} | ||
| > | ||
| {ns.title} | ||
| </Link> | ||
| </li> | ||
| ); | ||
| })} | ||
| {!loading && !error && namespaces.length === 0 && ( | ||
| <li className="block py-2.5 px-4 text-text-secondary border-b border-border"> | ||
| No namespaces | ||
| </li> | ||
| )} | ||
| </ul> | ||
| </Collapsible.Panel> | ||
| </Collapsible.Root> | ||
| <SidebarItemGroup | ||
| emptyLabel="No namespaces" | ||
| error={kvError} | ||
| icon={KVIcon} | ||
| items={namespaces.map((ns) => ({ | ||
| id: ns.id, | ||
| isActive: currentPath === `/kv/${ns.id}`, | ||
| label: ns.title, | ||
| link: { | ||
| params: { namespaceId: ns.id }, | ||
| to: "/kv/$namespaceId", | ||
| }, | ||
| }))} | ||
| loading={loading} | ||
| title="KV Namespaces" | ||
| /> | ||
|
|
||
| <SidebarItemGroup | ||
| emptyLabel="No databases" | ||
| error={d1Error} | ||
| icon={DatabaseIcon} | ||
| items={databases.map((db) => ({ | ||
| id: db.uuid as string, | ||
| isActive: currentPath === `/d1/${db.uuid}`, | ||
| label: db.name as string, | ||
| link: { | ||
| params: { databaseId: db.uuid }, | ||
| search: { table: undefined }, | ||
| to: "/d1/$databaseId", | ||
| }, | ||
| }))} | ||
| loading={loading} | ||
| title="D1 Databases" | ||
| /> | ||
| </aside> | ||
| ); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.