Skip to content
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

feat: table schema editor #11

Merged
merged 12 commits into from
Feb 25, 2024
322 changes: 215 additions & 107 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@
"@lezer/common": "^1.2.1",
"@lezer/lr": "^1.4.0",
"@libsql/hrana-client": "^0.5.5",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-context-menu": "^2.1.5",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-menubar": "^1.0.4",
"@radix-ui/react-navigation-menu": "^1.1.4",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
Expand All @@ -36,6 +40,7 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"cmdk": "^0.2.0",
"deep-equal": "^2.2.3",
"eslint-plugin-jest": "^27.6.3",
"lucide-react": "^0.309.0",
"next": "14.0.4",
Expand All @@ -49,6 +54,7 @@
"devDependencies": {
"@testing-library/jest-dom": "^6.2.1",
"@testing-library/react": "^14.1.2",
"@types/deep-equal": "^1.0.4",
"@types/jest": "^29.5.11",
"@types/node": "^20",
"@types/react": "^18",
Expand Down
39 changes: 20 additions & 19 deletions src/app/(components)/DatabaseGui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,14 @@ import useMessageListener from "@/hooks/useMessageListener";
import { MessageChannelName } from "@/messages/const";
import { OpenTabsProps } from "@/messages/openTabs";
import QueryWindow from "@/app/(windows)/QueryWindow";
import TopNavigation from "./TopNavigation";
import { appVersion } from "@/env";
import SchemaEditorTab from "@/screens/WindowTabs/SchemaEditorTab";

export default function DatabaseGui() {
const DEFAULT_WIDTH = 300;
const MAX_WIDTH = 400;

const [maxWidthPercentage, setMaxWidthPercentage] = useState(20);
const [defaultWidthPercentage, setDefaultWidthPercentage] = useState(20);

useEffect(() => {
setMaxWidthPercentage(Math.floor((MAX_WIDTH / window.innerWidth) * 100));
setDefaultWidthPercentage((DEFAULT_WIDTH / window.innerWidth) * 100);
}, []);

Expand Down Expand Up @@ -66,6 +62,24 @@ export default function DatabaseGui() {
component: <QueryWindow />,
},
];
} else if (newTab.type === "schema") {
// Check if there is duplicated
const foundIndex = prev.findIndex((tab) => tab.key === newTab.key);

if (foundIndex >= 0) {
setSelectedTabIndex(foundIndex);
} else {
setSelectedTabIndex(prev.length);

return [
...prev,
{
title: newTab.name,
key: newTab.key,
component: <SchemaEditorTab tableName={newTab.tableName} />,
},
];
}
}
}

Expand All @@ -76,21 +90,8 @@ export default function DatabaseGui() {

return (
<div className="h-screen w-screen flex flex-col">
<div className="flex border-b">
<div className="bg-blue-700 pr-2 pl-2 flex items-center text-white mr-2">
LibSQL <strong>Studio</strong>{" "}
<span className="text-xs ml-2">v{appVersion}</span>
</div>
<div className="p-1">
<TopNavigation />
</div>
</div>
<ResizablePanelGroup direction="horizontal">
<ResizablePanel
minSize={5}
maxSize={maxWidthPercentage || undefined}
defaultSize={defaultWidthPercentage}
>
<ResizablePanel minSize={5} defaultSize={defaultWidthPercentage}>
<SchemaView />
</ResizablePanel>
<ResizableHandle withHandle />
Expand Down
84 changes: 63 additions & 21 deletions src/app/(components)/SchemaView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { buttonVariants } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
import { openTabs } from "@/messages/openTabs";
import { LucideIcon, Table2 } from "lucide-react";
import { LucideIcon, LucideSearch, Table2 } from "lucide-react";
import { useCallback, useState } from "react";
import {
OpenContextMenuList,
openContextMenuFromEvent,
} from "@/messages/openContextMenu";
import { useSchema } from "@/screens/DatabaseScreen/SchemaProvider";
import { appVersion } from "@/env";

interface SchemaViewItemProps {
icon: LucideIcon;
Expand Down Expand Up @@ -70,33 +71,74 @@ export default function SchemaView() {
},
},
{ separator: true },
{
title: "Create New Table",
onClick: () => {
openTabs({
key: "_create_schema",
name: "Create Table",
type: "schema",
});
},
},
{
title: "Edit Table",
disabled: !tableName,
onClick: () => {
openTabs({
key: "_schema_" + tableName,
name: "Edit " + tableName,
tableName,
type: "schema",
});
},
},
{ separator: true },
{ title: "Refresh", onClick: () => refresh() },
] as OpenContextMenuList;
},
[refresh]
);

return (
<ScrollArea
className="h-full select-none"
onContextMenu={openContextMenuFromEvent(prepareContextMenu())}
>
<div className="flex flex-col p-2 pr-4">
{schema.map((item, schemaIndex) => {
return (
<SchemaViewmItem
onContextMenu={openContextMenuFromEvent(
prepareContextMenu(item.name)
)}
key={item.name}
title={item.name}
icon={Table2}
selected={schemaIndex === selectedIndex}
onClick={() => setSelectedIndex(schemaIndex)}
/>
);
})}
<div className="flex flex-col h-full overflow-hidden">
<div className="pt-2 px-2 flex h-10">
<div className="bg-secondary rounded overflow-hidden flex items-center ml-3 flex-grow">
<div className="text-sm px-2 h-full flex items-center">
<LucideSearch className="h-4 w-4 text-black" />
</div>
<input
type="text"
className="bg-inherit p-1 pl-2 pr-2 outline-none text-sm h-full flex-grow"
/>
</div>
</div>
<ScrollArea
className="flex-grow select-none"
onContextMenu={openContextMenuFromEvent(prepareContextMenu())}
>
<div className="flex flex-col p-2 pr-4">
{schema.map((item, schemaIndex) => {
return (
<SchemaViewmItem
onContextMenu={openContextMenuFromEvent(
prepareContextMenu(item.name)
)}
key={item.name}
title={item.name}
icon={Table2}
selected={schemaIndex === selectedIndex}
onClick={() => setSelectedIndex(schemaIndex)}
/>
);
})}
</div>
</ScrollArea>
<div className="bg-blue-700 h-8 flex items-center px-2 text-white">
<span>LibSQL</span>
<strong>Studio</strong>
<span className="text-xs ml-2">v{appVersion}</span>
</div>
</ScrollArea>
</div>
);
}
118 changes: 74 additions & 44 deletions src/app/(components)/WindowTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import openNewQuery from "@/messages/openNewQuery";
import { LucidePlus, LucideX } from "lucide-react";
import { createContext, useCallback, useContext, useMemo } from "react";

export interface WindowTabItemProps {
component: JSX.Element;
Expand All @@ -16,63 +17,92 @@ interface WindowTabsProps {
onTabsChange: (value: WindowTabItemProps[]) => void;
}

const WindowTabsContext = createContext<{
replaceCurrentTab: (tab: WindowTabItemProps) => void;
}>({
replaceCurrentTab: () => {
throw new Error("Not implemented");
},
});

export function useTabsContext() {
return useContext(WindowTabsContext);
}

export default function WindowTabs({
tabs,
selected,
onSelectChange,
onTabsChange,
}: WindowTabsProps) {
const replaceCurrentTab = useCallback(
(tab: WindowTabItemProps) => {
if (tabs[selected]) {
tabs[selected] = tab;
onTabsChange([...tabs]);
}
},
[tabs, selected, onTabsChange]
);

const contextValue = useMemo(
() => ({ replaceCurrentTab }),
[replaceCurrentTab]
);

return (
<div className="flex flex-col w-full h-full">
<div className="flex-grow-0 flex-shrink-0">
<div className="flex p-2 gap-2">
<Button
size={"sm"}
variant={"outline"}
onClick={() => {
openNewQuery();
}}
>
<LucidePlus className="w-4 h-4" />
</Button>
{tabs.map((tab, idx) => {
<WindowTabsContext.Provider value={contextValue}>
<div className="flex flex-col w-full h-full">
<div className="flex-grow-0 flex-shrink-0">
<div className="flex p-2 gap-2">
<Button
size={"sm"}
variant={"outline"}
onClick={() => {
openNewQuery();
}}
>
<LucidePlus className="w-4 h-4" />
</Button>
{tabs.map((tab, idx) => {
return (
<Button
size={"sm"}
key={tab.key}
variant={idx === selected ? "default" : "secondary"}
onClick={() => onSelectChange(idx)}
>
{tab.title}
<LucideX
className="w-4 h-4 ml-2"
onClick={() => {
onTabsChange(
tabs.filter((nextTab) => nextTab.key !== tab.key)
);
}}
/>
</Button>
);
})}
</div>
<Separator />
</div>
<div className="flex-grow relative">
{tabs.map((tab, tabIndex) => {
return (
<Button
size={"sm"}
<div
className="absolute left-0 right-0 top-0 bottom-0"
style={{
visibility: tabIndex === selected ? "visible" : "hidden",
}}
key={tab.key}
variant={idx === selected ? "default" : "secondary"}
onClick={() => onSelectChange(idx)}
>
{tab.title}
<LucideX
className="w-4 h-4 ml-2"
onClick={() => {
onTabsChange(
tabs.filter((nextTab) => nextTab.key !== tab.key)
);
}}
/>
</Button>
{tab.component}
</div>
);
})}
</div>
<Separator />
</div>
<div className="flex-grow relative">
{tabs.map((tab, tabIndex) => {
return (
<div
className="absolute left-0 right-0 top-0 bottom-0"
style={{
visibility: tabIndex === selected ? "visible" : "hidden",
}}
key={tab.key}
>
{tab.component}
</div>
);
})}
</div>
</div>
</WindowTabsContext.Provider>
);
}
13 changes: 11 additions & 2 deletions src/components/SqlEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ const theme = createTheme({

interface SqlEditorProps {
value: string;
onChange: (value: string) => void;
readOnly?: boolean;
onChange?: (value: string) => void;
schema?: Record<string, string[]>;
onKeyDown?: KeyboardEventHandler<HTMLDivElement>;
onCursorChange?: (
Expand All @@ -57,7 +58,14 @@ interface SqlEditorProps {

const SqlEditor = forwardRef<ReactCodeMirrorRef, SqlEditorProps>(
function SqlEditor(
{ value, onChange, schema, onKeyDown, onCursorChange }: SqlEditorProps,
{
value,
onChange,
schema,
onKeyDown,
onCursorChange,
readOnly,
}: SqlEditorProps,
ref
) {
const keyExtensions = useMemo(() => {
Expand All @@ -75,6 +83,7 @@ const SqlEditor = forwardRef<ReactCodeMirrorRef, SqlEditorProps>(
<CodeMirror
ref={ref}
autoFocus
readOnly={readOnly}
onKeyDown={onKeyDown}
basicSetup={{
defaultKeymap: false,
Expand Down
Loading
Loading