Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
4447f29
Added `@cloudflare/workers-editor-shared` dependency
NuroDev Feb 18, 2026
9cc8967
Minor `tokenizeSQL` refactoring
NuroDev Feb 18, 2026
887aca1
Added all codemirror & lezer dependencies
NuroDev Feb 18, 2026
f29806f
Moved `isEqual` to shared utilities
NuroDev Feb 18, 2026
f8c0dcd
Added shared studio commit utilities
NuroDev Feb 18, 2026
c558b9d
Added WIP table explorer tab components
NuroDev Feb 18, 2026
15b2767
Purged unused dependencies
NuroDev Feb 18, 2026
efb03a1
Added placeholder tab registry definitions
NuroDev Feb 18, 2026
3ec6b9e
Added changeset
NuroDev Feb 18, 2026
1d847db
Merge branch 'main' into NuroDev/local-explorer-studio-table-explorer
NuroDev Feb 18, 2026
c2f6234
Temp: Pruned where filter input + editor
NuroDev Feb 18, 2026
926e75f
Temp: Disabled WIP `StudioResultTable` component
NuroDev Feb 18, 2026
3dc5b78
Add TypeScript expect error comments
NuroDev Feb 18, 2026
138bdd1
Minor code cleanup
NuroDev Feb 18, 2026
845b16c
Temp: Removed `where-parser` utility
NuroDev Feb 18, 2026
870a9bb
Merge branch 'main' into NuroDev/local-explorer-studio-table-explorer
NuroDev Feb 18, 2026
f611b07
Minor commit confirmation modal refactoring
NuroDev Feb 18, 2026
d9369ae
Removed unused imports
NuroDev Feb 18, 2026
ae4df7f
Added TODO's for disabled variables
NuroDev Feb 18, 2026
408153a
Merge branch 'main' into NuroDev/local-explorer-studio-table-explorer
NuroDev Feb 18, 2026
9b05bb2
Import Kumo Tailwind CSS styles
NuroDev Feb 18, 2026
eb70210
Added custom Kumo style overrides
NuroDev Feb 18, 2026
7d38d58
Minor tab bar / footer style tweaks
NuroDev Feb 18, 2026
ef84629
Updated lockfile
NuroDev Feb 18, 2026
a6c98fb
Merge branch 'main' into NuroDev/local-explorer-studio-table-explorer
NuroDev Feb 18, 2026
75c3428
Merge branch 'main' into NuroDev/local-explorer-studio-table-explorer
NuroDev Feb 19, 2026
d0f2634
Merge branch 'main' into NuroDev/local-explorer-studio-table-explorer
NuroDev Feb 19, 2026
2035d7a
Merge branch 'main' into NuroDev/local-explorer-studio-table-explorer
NuroDev Feb 19, 2026
ece6bfd
Merge branch 'main' into NuroDev/local-explorer-studio-table-explorer
NuroDev Feb 19, 2026
a806a1a
Merge branch 'main' into NuroDev/local-explorer-studio-table-explorer
NuroDev Feb 19, 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/light-clocks-enter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@cloudflare/local-explorer-ui": minor
---

Adds the tab definition for the table explorer.

This serves as another stepping stone for adding the complete data studio to the local explorer.

This is a WIP experimental feature.
6 changes: 6 additions & 0 deletions packages/local-explorer-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@
"dependencies": {
"@base-ui/react": "^1.1.0",
"@cloudflare/kumo": "^1.5.0",
"@cloudflare/workers-editor-shared": "^0.1.1",
"@codemirror/autocomplete": "^6.20.0",
"@codemirror/lang-sql": "^6.10.0",
"@codemirror/language": "^6.12.1",
"@codemirror/state": "^6.5.4",
"@codemirror/view": "^6.39.14",
"@phosphor-icons/react": "^2.1.10",
"@tailwindcss/vite": "^4.0.15",
"@tanstack/react-router": "^1.158.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { describe, expect, test } from "vitest";
import {
escapeSqlValue,
tokenizeSQL,
transformStudioArrayBasedResult,
} from "../../utils/studio";
import { tokenizeSQL } from "../../utils/studio/sql";

describe("escapeSqlValue", () => {
test("undefined returns `DEFAULT`", () => {
Expand Down
21 changes: 21 additions & 0 deletions packages/local-explorer-ui/src/components/studio/Code/Block.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
interface CodeBlockProps {
code: string;
language?: string;
maxHeight?: number;
}

export function CodeBlock({
code,
language,
maxHeight,
}: CodeBlockProps): JSX.Element {
return (
<pre
className="rounded bg-surface-secondary p-3 text-sm font-mono overflow-x-auto overflow-y-auto"
data-language={language}
style={maxHeight ? { maxHeight } : undefined}
>
<code>{code}</code>
</pre>
);
}
180 changes: 180 additions & 0 deletions packages/local-explorer-ui/src/components/studio/Code/Mirror.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { Annotation, EditorState, StateEffect } from "@codemirror/state";
import {
EditorView,
placeholder as placeholderExtension,
} from "@codemirror/view";
import {
forwardRef,
useEffect,
useImperativeHandle,
useLayoutEffect,
useRef,
useState,
} from "react";
import type { Extension } from "@codemirror/state";
import type { ViewUpdate } from "@codemirror/view";

/**
* React binding for CodeMirror with minimal built-in extensions.
* Supports placeholder and content change events only.
*/
export const StudioCodeMirror = forwardRef<
StudioCodeMirrorReference,
StudioCodeMirrorProps
>(
(
{
autoFocus,
className,
defaultValue,
extensions,
onChange,
onCursorChange,
placeholder,
readOnly,
},
ref
) => {
const container = useRef<HTMLDivElement>(null);
const defaultValueRef = useRef<string>(defaultValue);

const [editorView, setEditorView] = useState<EditorView>();

useLayoutEffect(() => {
if (!container.current) {
return;
}

const view = new EditorView({
parent: container.current,
doc: defaultValueRef?.current,
});

setEditorView(view);

return () => view.destroy();
}, [container, defaultValueRef]);

// Registers new extensions with CodeMirror,
// including built-in support for placeholder and onChange events.
useEffect(() => {
if (!editorView) {
return;
}

const combinedExtensions = [...(extensions ?? [])] satisfies Extension[];

if (onChange) {
combinedExtensions.push(
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
if (
viewUpdate.docChanged &&
onChange &&
!viewUpdate.transactions.some((tr) =>
tr.annotation(BlockOnChangeTrigger)
)
) {
onChange(viewUpdate);
}
})
);
}

if (onCursorChange) {
combinedExtensions.push(
EditorView.updateListener.of((state) => {
const position = state.state.selection.main.head;
const line = state.state.doc.lineAt(position);
const lineNumber = line.number;
const columnNumber = position - line.from;
onCursorChange(position, lineNumber, columnNumber);
})
);
}

if (placeholder) {
combinedExtensions.push(placeholderExtension(placeholder));
}

if (readOnly) {
combinedExtensions.push(EditorState.readOnly.of(true));
}

editorView.dispatch({
effects: StateEffect.reconfigure.of(combinedExtensions),
});
}, [
editorView,
extensions,
onChange,
onCursorChange,
placeholder,
readOnly,
]);

// Exposes the CodeMirror editor instance and helper methods
// for getting and setting the editor content via ref.
useImperativeHandle(
ref,
() => ({
getValue: (): string => {
if (!editorView) {
return "";
}

return editorView.state.doc.toString();
},
setValue: (value: string): void => {
if (!editorView) {
return;
}

const currentValue = editorView.state.doc.toString();
editorView.dispatch({
annotations: [BlockOnChangeTrigger.of(true)],
changes: {
from: 0,
insert: value || "",
to: currentValue.length,
},
});
},
view: editorView,
}),
[editorView]
);

// Auto focus
useEffect((): void => {
if (autoFocus && editorView) {
editorView.focus();
}
}, [autoFocus, editorView]);

return <div ref={container} className={className}></div>;
}
);
StudioCodeMirror.displayName = "StudioCodeMirror";

const BlockOnChangeTrigger = Annotation.define<boolean>();

export interface StudioCodeMirrorProps {
autoFocus?: boolean;
className?: string;
defaultValue?: string;
extensions?: Extension[];
onChange?: (update: ViewUpdate) => void;
onCursorChange?: (
position: number,
lineNumber: number,
columnNumber: number
) => void;
placeholder?: string;
readOnly?: boolean;
}

export interface StudioCodeMirrorReference {
getValue: () => string;
setValue: (value: string) => void;
view?: EditorView;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Button, Dialog } from "@cloudflare/kumo";
import { PlayIcon, SpinnerIcon } from "@phosphor-icons/react";
import { useState } from "react";
import { CodeBlock } from "../Code/Block";

interface StudioCommitConfirmationProps {
closeModal: () => void;
isOpen: boolean;
onConfirm: () => Promise<void>;
statements: string[];
}

export function StudioCommitConfirmation({
closeModal,
isOpen,
onConfirm,
statements,
}: StudioCommitConfirmationProps) {
const [errorMessage, setErrorMessage] = useState("");
const [isRequesting, setIsRequesting] = useState(false);

const handleConfirm = async (): Promise<void> => {
setIsRequesting(true);
setErrorMessage("");

try {
await onConfirm();
closeModal();
} catch (err) {
if (err instanceof Error) {
setErrorMessage(err.message);
} else {
setErrorMessage(String(err));
}
} finally {
setIsRequesting(false);
}
};

return (
<Dialog.Root
open={isOpen}
onOpenChange={(open: boolean) => {
if (!open) {
closeModal();
}
}}
>
<Dialog>
{/* @ts-expect-error `@cloudflare/kumo` currently has a type def bug here */}
<Dialog.Title>Review and Confirm Changes</Dialog.Title>

<div className="flex flex-col gap-4 text-sm">
{!!errorMessage && (
<div className="font-mono text-red-500">{errorMessage}</div>
)}

<div>
The following SQL statements will be executed to apply your changes.
Please review them carefully before committing.
</div>

<CodeBlock
code={statements.join("\n")}
language="sql"
maxHeight={500}
/>
</div>

<div className="mt-4 flex justify-end gap-2">
<Button
disabled={isRequesting}
onClick={handleConfirm}
variant="primary"
>
{isRequesting ? (
<SpinnerIcon className="h-4 w-4 animate-spin" />
) : (
<PlayIcon className="h-4 w-4" />
)}
<span>Confirm & Execute</span>
</Button>
</div>
</Dialog>
</Dialog.Root>
);
}
Loading
Loading