Skip to content

Commit

Permalink
add notebook initial code
Browse files Browse the repository at this point in the history
  • Loading branch information
invisal committed Jan 14, 2025
1 parent 8bb3c6e commit 3cd4cd5
Show file tree
Hide file tree
Showing 15 changed files with 556 additions and 3 deletions.
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@libsql/client": "^0.5.3"
},
"dependencies": {
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-sql": "^6.5.5",
"@dagrejs/dagre": "^1.1.4",
Expand Down
113 changes: 113 additions & 0 deletions src/components/editor/javascript-editor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import CodeMirror, { ReactCodeMirrorRef } from "@uiw/react-codemirror";
import { javascript } from "@codemirror/lang-javascript";
import { forwardRef, useMemo } from "react";
import { tags as t } from "@lezer/highlight";
import createTheme from "@uiw/codemirror-themes";
import { useTheme } from "@/context/theme-provider";
import { indentationMarkers } from "@replit/codemirror-indentation-markers";

interface JsonEditorProps {
value: string;
readOnly?: boolean;
onChange?: (value: string) => void;
}

function useJavascriptTheme() {
const { theme } = useTheme();

return useMemo(() => {
if (theme === "light") {
return createTheme({
theme: "light",
settings: {
background: "#FFFFFF",
foreground: "#000000",
caret: "#FBAC52",
selection: "#FFD420",
selectionMatch: "#FFD420",
gutterBackground: "#fff",
gutterForeground: "#4D4D4C",
gutterBorder: "transparent",
lineHighlight: "#00000012",
fontFamily:
'Consolas, "Andale Mono", "Ubuntu Mono", "Courier New", monospace',
},
styles: [
{
tag: [t.propertyName, t.function(t.variableName)],
color: "#e67e22",
},
{ tag: [t.keyword], color: "#0000FF" },
{ tag: [t.comment, t.blockComment], color: "#95a5a6" },
{ tag: [t.bool, t.null], color: "#696C77" },
{ tag: [t.number], color: "#FF0080" },
{ tag: [t.string], color: "#50A14F" },
{ tag: [t.separator], color: "#383A42" },
{ tag: [t.squareBracket], color: "#383A42" },
{ tag: [t.brace], color: "#A626A4" },
],
});
} else {
return createTheme({
theme: "dark",
settings: {
background: "var(--background)",
foreground: "#9cdcfe",
caret: "#c6c6c6",
selection: "#6199ff2f",
selectionMatch: "#72a1ff59",
lineHighlight: "#ffffff0f",
gutterBackground: "var(--background)",
gutterForeground: "#838383",
gutterActiveForeground: "#fff",
fontFamily:
'Consolas, "Andale Mono", "Ubuntu Mono", "Courier New", monospace',
},
styles: [
{ tag: [t.propertyName], color: "#9b59b6" },
{ tag: [t.bool, t.null], color: "#696C77" },
{ tag: [t.number], color: "#f39c12" },
{ tag: [t.string], color: "#50A14F" },
{ tag: [t.separator], color: "#383A42" },
{ tag: [t.squareBracket], color: "#383A42" },
{ tag: [t.brace], color: "#A626A4" },
],
});
}
}, [theme]);
}

const JavascriptEditor = forwardRef<ReactCodeMirrorRef, JsonEditorProps>(
function JavascriptEditor(
{ value, onChange, readOnly }: JsonEditorProps,
ref
) {
const theme = useJavascriptTheme();

return (
<CodeMirror
className="border p-1 rounded"
ref={ref}
autoFocus
readOnly={readOnly}
basicSetup={{
drawSelection: false,
highlightActiveLine: false,
highlightActiveLineGutter: false,
foldGutter: false,
}}
theme={theme}
value={value}
height="100%"
onChange={onChange}
style={{
fontSize: 20,
height: "100%",
}}
extensions={[javascript(), indentationMarkers()]}
/>
);
}
);

export default JavascriptEditor;
3 changes: 2 additions & 1 deletion src/components/gui/database-gui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ export default function DatabaseGui() {
scc.tabs.openBuiltinQuery({});
},
},
...extensions.getWindowTabMenu(),
databaseDriver.getFlags().supportCreateUpdateTable
? {
text: "New Table",
Expand All @@ -143,7 +144,7 @@ export default function DatabaseGui() {
}
: undefined,
].filter(Boolean) as { text: string; onClick: () => void }[];
}, [currentSchemaName, databaseDriver]);
}, [currentSchemaName, databaseDriver, extensions]);

// Send to analytic when tab changes.
const previousLogTabKey = useRef<string>("");
Expand Down
2 changes: 1 addition & 1 deletion src/components/gui/schema-sidebar-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ export default function SchemaList({ search }: Readonly<SchemaListProps>) {
? {
title: "Edit Table",
onClick: () => {
scc.tabs.openBuiltinTable({
scc.tabs.openBuiltinSchema({
schemaName: item?.schemaName ?? currentSchemaName,
tableName: item?.name,
});
Expand Down
1 change: 1 addition & 0 deletions src/components/gui/sql-editor/use-editor-theme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export default function useCodeEditorTheme({
{ tag: [t.variableName], color: "#006600" },
{ tag: [t.escape], color: "#33CC33" },
{ tag: [t.tagName], color: "#1C02FF" },
{ tag: t.comment, color: "#bdc3c7" },
{ tag: [t.heading], color: "#0C07FF" },
{ tag: [t.quote], color: "#000000" },
{ tag: [t.list], color: "#B90690" },
Expand Down
10 changes: 10 additions & 0 deletions src/core/extension-manager.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ReactElement } from "react";
import { IStudioExtension } from "./extension-base";
import { ExtensionMenuItem } from "./extension-menu";

interface RegisterSidebarOption {
key: string;
Expand Down Expand Up @@ -51,6 +52,7 @@ export class StudioExtensionManager {
private sidebars: RegisterSidebarOption[] = [];
private beforeQueryHandlers: BeforeQueryHandler[] = [];
private afterQueryHandlers: AfterQueryHandler[] = [];
private windowTabMenu: ExtensionMenuItem[] = [];

constructor(private extensions: IStudioExtension[]) {}

Expand Down Expand Up @@ -78,6 +80,14 @@ export class StudioExtensionManager {
this.afterQueryHandlers.push(handler);
}

registerWindowTabMenu(menu: ExtensionMenuItem) {
this.windowTabMenu.push(menu);
}

getWindowTabMenu(): Readonly<ExtensionMenuItem[]> {
return this.windowTabMenu;
}

async beforeQuery(payload: BeforeQueryPipeline) {
for (const handler of this.beforeQueryHandlers) {
await handler(payload);
Expand Down
4 changes: 4 additions & 0 deletions src/core/extension-menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface ExtensionMenuItem {
text: string;
onClick: () => void;
}
3 changes: 2 additions & 1 deletion src/core/standard-extension.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
* This contains the standard extensions as a base for all databases.
*/

import NotebookExtension from "@/extensions/notebook";
import QueryHistoryConsoleLogExtension from "@/extensions/query-console-log";

export function createStandardExtensions() {
return [new QueryHistoryConsoleLogExtension()];
return [new QueryHistoryConsoleLogExtension(), new NotebookExtension()];
}
28 changes: 28 additions & 0 deletions src/extensions/notebook/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { StudioExtension } from "@/core/extension-base";
import { StudioExtensionManager } from "@/core/extension-manager";
import { createTabExtension } from "@/core/extension-tab";
import { NotebookIcon } from "lucide-react";
import NotebookTab from "./notebook-tab";

const notebookTab = createTabExtension({
name: "notebook",
key: () => "notebook",
generate: () => ({
title: "Notebook",
component: <NotebookTab />,
icon: NotebookIcon,
}),
});

export default class NotebookExtension extends StudioExtension {
extensionName = "notebook";

init(studio: StudioExtensionManager): void {
studio.registerWindowTabMenu({
text: "Notebook",
onClick: () => {
notebookTab.open({});
},
});
}
}
116 changes: 116 additions & 0 deletions src/extensions/notebook/notebook-block-code.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { useMemo, useState } from "react";
import { NotebookEditorBlockValue } from "./notebook-editor";
import { NotebookVM } from "./notebook-vm";
import JavascriptEditor from "@/components/editor/javascript-editor";
import { Button } from "@/components/ui/button";
import { PlayIcon, Terminal } from "lucide-react";
import { produce } from "immer";

interface OutputFormat {
type: "log";
args: unknown[];
}

function OutputArgItem({ value }: { value: unknown }) {
const content = useMemo(() => {
if (typeof value === "object") {
return JSON.stringify(
value,
(_, propValue) => {
if (typeof propValue === "function") {
return propValue.toString();
} else if (typeof propValue === "bigint") {
return propValue.toString() + "n";
}

return propValue;
},
2
);
}

return (value ?? "").toString();
}, []);

if (content.length > 500) {
return (
<span className="mr-2 text-blue-500 font-bold">[Output too long]</span>
);
}

return <span className="mr-2">{content}</span>;
}

function OutputItem({ value }: { value: OutputFormat }) {
return (
<div>
<pre>
{value.args.map((argValue, argIndex) => (
<OutputArgItem value={argValue} key={argIndex} />
))}
</pre>
</div>
);
}

export default function NotebookBlockCode({
value,
onChange,
vm,
}: {
vm: NotebookVM;
value: NotebookEditorBlockValue;
onChange: (value: NotebookEditorBlockValue) => void;
}) {
const [output, setOutput] = useState<OutputFormat[]>([]);

const onRunClick = () => {
setOutput([]);
vm.run(value.value, {
complete: () => {
console.log("Complete");
},
stdOut: (data: any) => {
setOutput((prev) => [...prev, data]);
},
stdErr: () => {
console.log("Error");
},
});
};

const onClearLogClick = () => {
setOutput([]);
};

return (
<div className="flex-1 flex flex-col p-3 gap-2">
<div className="flex gap-2">
<Button variant={"outline"} onClick={onRunClick}>
<PlayIcon className="w-4 h-4 mr-2" /> Run
</Button>

<Button variant={"outline"} onClick={onClearLogClick}>
<Terminal className="w-4 h-4 mr-2" /> Clear Log
</Button>
</div>

<JavascriptEditor
value={value.value}
onChange={(e) => {
onChange(
produce(value, (draft) => {
draft.value = e;
})
);
}}
/>

<div>
{output.map((outputContent, outIdx) => (
<OutputItem key={outIdx} value={outputContent} />
))}
</div>
</div>
);
}
17 changes: 17 additions & 0 deletions src/extensions/notebook/notebook-block-md.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { compile } from "@mdx-js/mdx";
import { useEffect, useMemo, useState } from "react";
import { NotebookEditorBlockValue } from "./notebook-editor";

export default function NotebookBlockCode({
value,
onChange,
}: {
value: NotebookEditorBlockValue;
onChange: (value: NotebookEditorBlockValue) => void;
}) {
return (
<div className="p-3">
<textarea className="w-full resize-none" value={value.value} readOnly />
</div>
);
}
Loading

0 comments on commit 3cd4cd5

Please sign in to comment.