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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/dependabot-update-12768.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"miniflare": patch
"wrangler": patch
---

Update dependencies of "miniflare", "wrangler"

The following dependency versions have been updated:

| Dependency | From | To |
| ---------- | ------------ | ------------ |
| workerd | 1.20260301.1 | 1.20260305.1 |
9 changes: 9 additions & 0 deletions .changeset/young-facts-grin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@cloudflare/local-explorer-ui": minor
---

Add schema editor to data studio

Adds a visual schema editor to the data studio that allows you to create new database tables and edit existing table schemas. The editor provides column management (add, edit, remove), constraint editing (primary keys, unique constraints), and generates the corresponding SQL statements for review before committing changes.

This is a WIP experimental feature.
1 change: 1 addition & 0 deletions packages/local-explorer-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@phosphor-icons/react": "^2.1.10",
"@tailwindcss/vite": "^4.0.15",
"@tanstack/react-router": "^1.158.0",
"immer": "^11.1.4",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwindcss": "^4.0.15"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ function describeExplainNode(d: string): {
label: (
<div className="flex items-center">
<strong>SCAN </strong>
<span className="border border-color p-1 mx-2 rounded flex items-center gap-2">
<span className="border border-border p-1 mx-2 rounded flex items-center gap-2">
<TableIcon />
{d.substring("SCAN ".length)}
</span>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { Button, Dialog, Text } from "@cloudflare/kumo";
import { useState } from "react";
import type { IStudioDriver } from "../../../types/studio";
import type { SubmitEvent } from "react";

interface DropTableConfirmationModalProps {
closeModal: () => void;
driver: IStudioDriver;
isOpen: boolean;
onSuccess?: () => void;
schemaName: string;
tableName: string;
}

export function DropTableConfirmationModal({
closeModal,
driver,
isOpen,
onSuccess,
schemaName,
tableName,
}: DropTableConfirmationModalProps): JSX.Element {
const [challengeInput, setChallengeInput] = useState<string>("");
const [error, setError] = useState<string | null>(null);
const [isDeleting, setIsDeleting] = useState<boolean>(false);

const isValid = challengeInput === tableName;

const handleSubmit = async (e: SubmitEvent<HTMLFormElement>) => {
e.preventDefault();
setIsDeleting(true);
setError(null);

try {
await driver.dropTable(schemaName, tableName);
onSuccess?.();
closeModal();
} catch (err) {
setIsDeleting(false);
setError(err instanceof Error ? err.message : "Failed to delete table");
}
};

return (
<Dialog.Root
onOpenChange={(open: boolean) => {
if (!open) {
closeModal();
}
}}
open={isOpen}
>
<Dialog className="p-6">
<div className="flex items-start justify-between gap-4 mb-4">
{/* @ts-expect-error - Type mismatch due to pnpm monorepo @types/react version conflict */}
<Dialog.Title className="text-lg font-semibold">
Delete table?
</Dialog.Title>
</div>

<form onSubmit={handleSubmit}>
<div className="space-y-4">
{/* @ts-expect-error - Type mismatch due to pnpm monorepo @types/react version conflict */}
<Dialog.Description className="text-kumo-subtle">
Are you sure you want to delete the table{" "}
<strong>{tableName}</strong>? This action cannot be undone.
</Dialog.Description>

<div className="space-y-2">
<Text size="sm">
Type <strong>{tableName}</strong> to confirm
</Text>
<input
autoComplete="off"
autoFocus
className="w-full rounded-md border border-border bg-transparent px-3 py-2 text-sm"
onChange={(e) => setChallengeInput(e.target.value)}
value={challengeInput}
/>
</div>

{error && (
<div className="rounded-md bg-red-50 p-3 text-red-700">
{error}
</div>
)}
</div>

<div className="flex gap-2 justify-end mt-4">
<Button onClick={closeModal} variant="secondary">
Cancel
</Button>

<Button
disabled={!isValid || isDeleting}
loading={isDeleting}
type="submit"
variant="destructive"
>
Delete
</Button>
</div>
</form>
</Dialog>
</Dialog.Root>
);
}
40 changes: 40 additions & 0 deletions packages/local-explorer-ui/src/components/studio/SkeletonBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { cn } from "@cloudflare/kumo";
import type { HTMLAttributes } from "react";

interface SkeletonBlockProps extends HTMLAttributes<HTMLDivElement> {
height?: number | string;
mb?: number;
p?: number;
width?: number | string;
}

export function SkeletonBlock({
className,
height,
mb,
p,
style,
width,
...props
}: SkeletonBlockProps) {
return (
<div
className={cn(
"relative overflow-hidden rounded-md bg-surface-tertiary",
"before:absolute before:inset-0 before:animate-pulse before:bg-linear-to-r before:from-transparent before:via-white/20 before:to-transparent",
className
)}
style={{
height: typeof height === "number" ? `${height}px` : height,
width: typeof width === "number" ? `${width}px` : width,
marginBottom: mb ? `${mb * 4}px` : undefined,
padding: p ? `${p * 4}px` : undefined,
...style,
}}
{...props}
>
{/* Non-breaking space for sizing */}
&nbsp;
</div>
);
}
29 changes: 27 additions & 2 deletions packages/local-explorer-ui/src/components/studio/TabRegister.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { BinocularsIcon, TableIcon } from "@phosphor-icons/react";
import { BinocularsIcon, PencilIcon, TableIcon } from "@phosphor-icons/react";
import { StudioCreateUpdateTableTab } from "./Tabs/CreateUpdateTable";
import { StudioQueryTab } from "./Tabs/Query";
import { StudioTableExplorerTab } from "./Tabs/TableExplorer";
import type { Icon } from "@phosphor-icons/react";
Expand Down Expand Up @@ -26,7 +27,31 @@ const TableTab: TabDefinition<{
type: "table",
};

const RegisteredTabDefinition = [QueryTab, TableTab];
const EditTableTab: TabDefinition<{
schemaName: string;
tableName: string;
type: "edit-table";
}> = {
icon: PencilIcon,
makeComponent: ({ schemaName, tableName }) => (
<StudioCreateUpdateTableTab schemaName={schemaName} tableName={tableName} />
),
makeIdentifier: (tab) => `edit-table/${tab.schemaName}.${tab.tableName}`,
makeTitle: ({ tableName }) => tableName,
type: "edit-table",
};

const NewTableTab: TabDefinition<{
type: "create-table";
}> = {
icon: PencilIcon,
makeComponent: () => <StudioCreateUpdateTableTab />,
makeIdentifier: () => `create-table`,
makeTitle: () => "Create table",
type: "create-table",
};

const RegisteredTabDefinition = [QueryTab, TableTab, EditTableTab, NewTableTab];

export interface TabDefinition<T extends { type: string }> {
icon: Icon;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Button, DropdownMenu } from "@cloudflare/kumo";
import { CopyIcon, TableIcon, TextTIcon } from "@phosphor-icons/react";
import { useCallback } from "react";
import type { IStudioDriver } from "../../../types/studio";

export interface TableTarget {
schemaName: string;
tableName: string;
}

interface TableActionsDropdownProps {
/**
* The currently selected table. When null/undefined, the dropdown trigger is disabled.
*/
currentTable: string | null | undefined;

/**
* The database driver used to perform operations like fetching schema and dropping tables.
*/
driver: IStudioDriver;

/**
* The schema name for the current table.
*
* @default 'main'
*/
schemaName?: string;
}

export function StudioTableActionsDropdown({
currentTable,
driver,
schemaName = "main",
}: TableActionsDropdownProps): JSX.Element {
const handleCopyTableName = useCallback(async (): Promise<void> => {
if (!currentTable) {
return;
}

await window.navigator.clipboard.writeText(currentTable);
}, [currentTable]);

const handleCopyTableSchema = useCallback(async (): Promise<void> => {
if (!currentTable) {
return;
}

const tableSchema = await driver.tableSchema(schemaName, currentTable);
if (!tableSchema.createScript) {
return;
}

await window.navigator.clipboard.writeText(tableSchema.createScript);
}, [currentTable, driver, schemaName]);

return (
<>
<DropdownMenu>
<DropdownMenu.Trigger
render={
<Button
aria-label="Copy"
disabled={!currentTable}
icon={CopyIcon}
shape="square"
/>
}
/>

<DropdownMenu.Content>
<DropdownMenu.Item
className="space-x-2 cursor-pointer"
icon={TextTIcon}
onClick={handleCopyTableName}
>
Copy table name
</DropdownMenu.Item>

<DropdownMenu.Item
className="space-x-2 cursor-pointer"
icon={TableIcon}
onClick={handleCopyTableSchema}
>
Copy table schema
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu>
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ function InputCellEditor({
{popover &&
createPortal(
<div
className="bg-surface border border-color rounded fixed shadow flex flex-col"
className="bg-surface border border-border rounded fixed shadow flex flex-col"
ref={refs.setFloating}
style={{
...(floatingStyles as React.CSSProperties),
Expand Down
Loading
Loading