@sqlrooms/sql-editor 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-dev.log +7 -0
- package/.turbo/turbo-lint.log +16 -0
- package/CHANGELOG.md +8 -0
- package/LICENSE.md +9 -0
- package/dist/CreateTableModal.d.ts +12 -0
- package/dist/CreateTableModal.d.ts.map +1 -0
- package/dist/CreateTableModal.js +51 -0
- package/dist/DeleteSqlQueryModal.d.ts +9 -0
- package/dist/DeleteSqlQueryModal.d.ts.map +1 -0
- package/dist/DeleteSqlQueryModal.js +6 -0
- package/dist/RenameSqlQueryModal.d.ts +10 -0
- package/dist/RenameSqlQueryModal.d.ts.map +1 -0
- package/dist/RenameSqlQueryModal.js +26 -0
- package/dist/SqlEditor.d.ts +15 -0
- package/dist/SqlEditor.d.ts.map +1 -0
- package/dist/SqlEditor.js +206 -0
- package/dist/SqlEditorModal.d.ts +5 -0
- package/dist/SqlEditorModal.d.ts.map +1 -0
- package/dist/SqlEditorModal.js +11 -0
- package/dist/TablesList.d.ts +12 -0
- package/dist/TablesList.d.ts.map +1 -0
- package/dist/TablesList.js +8 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/eslint.config.js +4 -0
- package/package.json +41 -0
- package/src/CreateTableModal.tsx +161 -0
- package/src/DeleteSqlQueryModal.tsx +42 -0
- package/src/RenameSqlQueryModal.tsx +92 -0
- package/src/SqlEditor.tsx +495 -0
- package/src/SqlEditorModal.tsx +31 -0
- package/src/TablesList.tsx +50 -0
- package/src/index.ts +4 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
|
|
2
|
+
> @sqlrooms/sql-editor@0.0.0 lint /Users/ilya/Workspace/sqlrooms/packages/sql-editor
|
|
3
|
+
> eslint .
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
/Users/ilya/Workspace/sqlrooms/packages/sql-editor/src/CreateTableModal.tsx
|
|
7
|
+
89:10 warning Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment
|
|
8
|
+
114:15 warning Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment
|
|
9
|
+
129:15 warning Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment
|
|
10
|
+
|
|
11
|
+
/Users/ilya/Workspace/sqlrooms/packages/sql-editor/src/RenameSqlQueryModal.tsx
|
|
12
|
+
58:10 warning Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment
|
|
13
|
+
62:15 warning Use "@ts-expect-error" instead of "@ts-ignore", as "@ts-ignore" will do nothing if the following line is error-free @typescript-eslint/ban-ts-comment
|
|
14
|
+
|
|
15
|
+
✖ 5 problems (0 errors, 5 warnings)
|
|
16
|
+
|
package/CHANGELOG.md
ADDED
package/LICENSE.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright 2025 Ilya Boyandin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { SqlQueryDataSource } from '@sqlrooms/project-config';
|
|
2
|
+
import { FC } from 'react';
|
|
3
|
+
export type Props = {
|
|
4
|
+
query: string;
|
|
5
|
+
isOpen: boolean;
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
editDataSource?: SqlQueryDataSource;
|
|
8
|
+
onAddOrUpdateSqlQuery: (tableName: string, query: string, oldTableName?: string) => Promise<void>;
|
|
9
|
+
};
|
|
10
|
+
declare const CreateTableModal: FC<Props>;
|
|
11
|
+
export default CreateTableModal;
|
|
12
|
+
//# sourceMappingURL=CreateTableModal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CreateTableModal.d.ts","sourceRoot":"","sources":["../src/CreateTableModal.tsx"],"names":[],"mappings":"AAoBA,OAAO,EACL,kBAAkB,EAEnB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAC,EAAE,EAAc,MAAM,OAAO,CAAC;AAgBtC,MAAM,MAAM,KAAK,GAAG;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,cAAc,CAAC,EAAE,kBAAkB,CAAC;IACpC,qBAAqB,EAAE,CACrB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,YAAY,CAAC,EAAE,MAAM,KAClB,OAAO,CAAC,IAAI,CAAC,CAAC;CACpB,CAAC;AAEF,QAAA,MAAM,gBAAgB,EAAE,EAAE,CAAC,KAAK,CA0G/B,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Form, FormControl, FormField, FormItem, FormLabel, FormMessage, Input, Textarea, Alert, AlertDescription, } from '@sqlrooms/ui';
|
|
3
|
+
import { DuckQueryError } from '@sqlrooms/duckdb';
|
|
4
|
+
import { VALID_TABLE_OR_COLUMN_REGEX, } from '@sqlrooms/project-config';
|
|
5
|
+
import { useCallback } from 'react';
|
|
6
|
+
import { useForm } from 'react-hook-form';
|
|
7
|
+
import * as z from 'zod';
|
|
8
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
9
|
+
const formSchema = z.object({
|
|
10
|
+
tableName: z
|
|
11
|
+
.string()
|
|
12
|
+
.min(1, 'Table name is required')
|
|
13
|
+
.regex(VALID_TABLE_OR_COLUMN_REGEX, 'Only letters, digits and underscores are allowed; should not start with a digit'),
|
|
14
|
+
query: z.string().min(1, 'Query is required'),
|
|
15
|
+
});
|
|
16
|
+
const CreateTableModal = (props) => {
|
|
17
|
+
const { editDataSource, isOpen, onClose, onAddOrUpdateSqlQuery } = props;
|
|
18
|
+
const form = useForm({
|
|
19
|
+
resolver: zodResolver(formSchema),
|
|
20
|
+
defaultValues: {
|
|
21
|
+
tableName: editDataSource?.tableName ?? '',
|
|
22
|
+
query: editDataSource?.sqlQuery ?? props.query.trim(),
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
const onSubmit = useCallback(async (values) => {
|
|
26
|
+
try {
|
|
27
|
+
const { tableName, query } = values;
|
|
28
|
+
await onAddOrUpdateSqlQuery(tableName, query, editDataSource?.tableName);
|
|
29
|
+
form.reset();
|
|
30
|
+
onClose();
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
form.setError('root', {
|
|
34
|
+
type: 'manual',
|
|
35
|
+
message: err instanceof DuckQueryError ? err.getMessageForUser() : `${err}`,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}, [onAddOrUpdateSqlQuery, editDataSource?.tableName, onClose, form]);
|
|
39
|
+
return (_jsx(Dialog, { open: isOpen, onOpenChange: (open) => !open && onClose(), children: _jsx(DialogContent, { className: "sm:max-w-[800px]", children: _jsx(Form, { ...form, children: _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), className: "space-y-4", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: editDataSource
|
|
40
|
+
? 'Edit table query'
|
|
41
|
+
: 'Create table from query' }), !editDataSource && (_jsx(DialogDescription, { children: "Create a new table from the results of an SQL query." }))] }), form.formState.errors.root && (_jsx(Alert, { variant: "destructive", children: _jsx(AlertDescription, { children: form.formState.errors.root.message }) })), _jsx(FormField
|
|
42
|
+
// @ts-ignore
|
|
43
|
+
, {
|
|
44
|
+
// @ts-ignore
|
|
45
|
+
control: form.control, name: "tableName", render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormLabel, { children: "Table name:" }), _jsx(FormControl, { children: _jsx(Input, { ...field, className: "font-mono", autoFocus: true }) }), _jsx(FormMessage, {})] })) }), _jsx(FormField
|
|
46
|
+
// @ts-ignore
|
|
47
|
+
, {
|
|
48
|
+
// @ts-ignore
|
|
49
|
+
control: form.control, name: "query", render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormLabel, { children: "SQL query:" }), _jsx(FormControl, { children: _jsx(Textarea, { ...field, className: "font-mono text-sm bg-secondary min-h-[200px]" }) }), _jsx(FormMessage, {})] })) }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "outline", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "submit", disabled: form.formState.isSubmitting, children: editDataSource ? 'Update' : 'Create' })] })] }) }) }) }));
|
|
50
|
+
};
|
|
51
|
+
export default CreateTableModal;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DeleteSqlQueryModal.d.ts","sourceRoot":"","sources":["../src/DeleteSqlQueryModal.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,UAAU,KAAK;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,SAAS,EAAE,MAAM,IAAI,CAAC;CACvB;AAED,QAAA,MAAM,mBAAmB,EAAE,KAAK,CAAC,EAAE,CAAC,KAAK,CAsBxC,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Button, } from '@sqlrooms/ui';
|
|
3
|
+
const DeleteSqlQueryModal = ({ isOpen, onClose, onConfirm }) => {
|
|
4
|
+
return (_jsx(Dialog, { open: isOpen, onOpenChange: (open) => !open && onClose(), children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Delete Query" }), _jsx(DialogDescription, { children: "Are you sure you want to delete this query? This action cannot be undone." })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: onClose, children: "Cancel" }), _jsx(Button, { variant: "destructive", onClick: onConfirm, children: "Delete" })] })] }) }));
|
|
5
|
+
};
|
|
6
|
+
export default DeleteSqlQueryModal;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface Props {
|
|
3
|
+
isOpen: boolean;
|
|
4
|
+
onClose: () => void;
|
|
5
|
+
initialName: string;
|
|
6
|
+
onRename: (newName: string) => void;
|
|
7
|
+
}
|
|
8
|
+
declare const RenameSqlQueryModal: React.FC<Props>;
|
|
9
|
+
export default RenameSqlQueryModal;
|
|
10
|
+
//# sourceMappingURL=RenameSqlQueryModal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RenameSqlQueryModal.d.ts","sourceRoot":"","sources":["../src/RenameSqlQueryModal.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,MAAM,OAAO,CAAC;AAW1B,UAAU,KAAK;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC;AAED,QAAA,MAAM,mBAAmB,EAAE,KAAK,CAAC,EAAE,CAAC,KAAK,CAwDxC,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Button, Form, FormControl, FormField, FormItem, FormLabel, FormMessage, Input, } from '@sqlrooms/ui';
|
|
3
|
+
import { useForm } from 'react-hook-form';
|
|
4
|
+
import * as z from 'zod';
|
|
5
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
6
|
+
const formSchema = z.object({
|
|
7
|
+
queryName: z.string().min(1, 'Query name is required'),
|
|
8
|
+
});
|
|
9
|
+
const RenameSqlQueryModal = ({ isOpen, onClose, initialName, onRename, }) => {
|
|
10
|
+
const form = useForm({
|
|
11
|
+
resolver: zodResolver(formSchema),
|
|
12
|
+
defaultValues: {
|
|
13
|
+
queryName: initialName,
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
function onSubmit(values) {
|
|
17
|
+
onRename(values.queryName);
|
|
18
|
+
onClose();
|
|
19
|
+
}
|
|
20
|
+
return (_jsx(Dialog, { open: isOpen, onOpenChange: (open) => !open && onClose(), children: _jsxs(DialogContent, { children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: "Rename Query" }) }), _jsx(Form, { ...form, children: _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), className: "space-y-4", children: [_jsx(FormField
|
|
21
|
+
// @ts-ignore
|
|
22
|
+
, {
|
|
23
|
+
// @ts-ignore
|
|
24
|
+
control: form.control, name: "queryName", render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormLabel, { children: "Query Name" }), _jsx(FormControl, { children: _jsx(Input, { ...field, autoFocus: true, placeholder: "Enter query name" }) }), _jsx(FormMessage, {})] })) }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "outline", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "submit", children: "Save" })] })] }) })] }) }));
|
|
25
|
+
};
|
|
26
|
+
export default RenameSqlQueryModal;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { SqlEditorConfig } from '@sqlrooms/project-config';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Props as CreateTableModalProps } from './CreateTableModal';
|
|
4
|
+
export type Props = {
|
|
5
|
+
schema?: string;
|
|
6
|
+
isOpen: boolean;
|
|
7
|
+
documentationPanel?: React.ReactNode;
|
|
8
|
+
sqlEditorConfig: SqlEditorConfig;
|
|
9
|
+
onChange: (config: SqlEditorConfig) => void;
|
|
10
|
+
onClose: () => void;
|
|
11
|
+
onAddOrUpdateSqlQuery: CreateTableModalProps['onAddOrUpdateSqlQuery'];
|
|
12
|
+
};
|
|
13
|
+
declare const SqlEditor: React.FC<Props>;
|
|
14
|
+
export default SqlEditor;
|
|
15
|
+
//# sourceMappingURL=SqlEditor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SqlEditor.d.ts","sourceRoot":"","sources":["../src/SqlEditor.tsx"],"names":[],"mappings":"AAWA,OAAO,EAAC,eAAe,EAAC,MAAM,0BAA0B,CAAC;AA4BzD,OAAO,KAAiD,MAAM,OAAO,CAAC;AACtE,OAAyB,EACvB,KAAK,IAAI,qBAAqB,EAC/B,MAAM,oBAAoB,CAAC;AAO5B,MAAM,MAAM,KAAK,GAAG;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,kBAAkB,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACrC,eAAe,EAAE,eAAe,CAAC;IACjC,QAAQ,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAC;IAC5C,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,qBAAqB,EAAE,qBAAqB,CAAC,uBAAuB,CAAC,CAAC;CACvE,CAAC;AAEF,QAAA,MAAM,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC,KAAK,CAib9B,CAAC;AAEF,eAAe,SAAS,CAAC"}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { DataTableVirtualized, QueryDataTable, useArrowDataTable, } from '@sqlrooms/data-table';
|
|
3
|
+
import { DuckQueryError, escapeId, getDuckTables, useDuckDb, } from '@sqlrooms/duckdb';
|
|
4
|
+
import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, SpinnerPane, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, ResizablePanelGroup, ResizablePanel, ResizableHandle, } from '@sqlrooms/ui';
|
|
5
|
+
import { genRandomStr, generateUniqueName } from '@sqlrooms/utils';
|
|
6
|
+
import { csvFormat } from 'd3-dsv';
|
|
7
|
+
import { saveAs } from 'file-saver';
|
|
8
|
+
import { BookOpenIcon, DownloadIcon, MoreVerticalIcon, PlayIcon, PlusIcon, } from 'lucide-react';
|
|
9
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
10
|
+
import CreateTableModal from './CreateTableModal';
|
|
11
|
+
import DeleteSqlQueryModal from './DeleteSqlQueryModal';
|
|
12
|
+
import RenameSqlQueryModal from './RenameSqlQueryModal';
|
|
13
|
+
import { TablesList } from './TablesList';
|
|
14
|
+
const DEFAULT_QUERY = '';
|
|
15
|
+
const SqlEditor = (props) => {
|
|
16
|
+
const { schema = 'main', documentationPanel, onAddOrUpdateSqlQuery, sqlEditorConfig, onChange, } = props;
|
|
17
|
+
const duckConn = useDuckDb();
|
|
18
|
+
const [showDocs, setShowDocs] = useState(false);
|
|
19
|
+
const [tables, setTables] = useState([]);
|
|
20
|
+
const [tablesLoading, setTablesLoading] = useState(false);
|
|
21
|
+
const [tablesError, setTablesError] = useState(null);
|
|
22
|
+
const [results, setResults] = useState();
|
|
23
|
+
const resultsTableData = useArrowDataTable(results);
|
|
24
|
+
const [loading, setLoading] = useState(false);
|
|
25
|
+
const [selectedTable, setSelectedTable] = useState();
|
|
26
|
+
const [error, setError] = useState(null);
|
|
27
|
+
const fetchTables = useCallback(async () => {
|
|
28
|
+
if (!duckConn.conn)
|
|
29
|
+
return;
|
|
30
|
+
try {
|
|
31
|
+
setTablesLoading(true);
|
|
32
|
+
setTablesError(null);
|
|
33
|
+
const tablesList = await getDuckTables(schema);
|
|
34
|
+
setTables(tablesList);
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
console.error(e);
|
|
38
|
+
setTablesError(e);
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
setTablesLoading(false);
|
|
42
|
+
}
|
|
43
|
+
}, [duckConn.conn, schema]);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
void fetchTables();
|
|
46
|
+
}, [fetchTables]);
|
|
47
|
+
const runQuery = async (q) => {
|
|
48
|
+
const conn = duckConn.conn;
|
|
49
|
+
try {
|
|
50
|
+
setError(null);
|
|
51
|
+
setLoading(true);
|
|
52
|
+
await conn.query(`SET search_path = ${schema}`);
|
|
53
|
+
const results = await conn.query(q);
|
|
54
|
+
await conn.query(`SET search_path = main`);
|
|
55
|
+
setResults(results);
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
setResults(undefined);
|
|
59
|
+
setError((e instanceof DuckQueryError
|
|
60
|
+
? e.getMessageForUser()
|
|
61
|
+
: 'Query failed') ?? e);
|
|
62
|
+
console.error(e);
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
setLoading(false);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const handleSelectTable = (table) => {
|
|
69
|
+
setSelectedTable(table);
|
|
70
|
+
};
|
|
71
|
+
const handleRunQuery = async () => {
|
|
72
|
+
setSelectedTable(undefined);
|
|
73
|
+
const textarea = document.querySelector(`textarea[id="${sqlEditorConfig.selectedQueryId}"]`);
|
|
74
|
+
const selectedText = textarea instanceof HTMLTextAreaElement
|
|
75
|
+
? textarea?.value.substring(textarea.selectionStart, textarea.selectionEnd)
|
|
76
|
+
: undefined;
|
|
77
|
+
const queryToRun = selectedText || currentQuery;
|
|
78
|
+
await runQuery(queryToRun);
|
|
79
|
+
void fetchTables();
|
|
80
|
+
};
|
|
81
|
+
const getQueryIndexById = (id) => {
|
|
82
|
+
return sqlEditorConfig.queries.findIndex((q) => q.id === id);
|
|
83
|
+
};
|
|
84
|
+
const getCurrentQueryIndex = () => {
|
|
85
|
+
return getQueryIndexById(sqlEditorConfig.selectedQueryId);
|
|
86
|
+
};
|
|
87
|
+
const handleTabChange = (value) => {
|
|
88
|
+
onChange({
|
|
89
|
+
...sqlEditorConfig,
|
|
90
|
+
selectedQueryId: value,
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
const handleUpdateQuery = (e) => {
|
|
94
|
+
if (!sqlEditorConfig)
|
|
95
|
+
return;
|
|
96
|
+
const currentIndex = getCurrentQueryIndex();
|
|
97
|
+
const newQueries = [...sqlEditorConfig.queries];
|
|
98
|
+
if (!newQueries[currentIndex])
|
|
99
|
+
return;
|
|
100
|
+
newQueries[currentIndex] = {
|
|
101
|
+
...newQueries[currentIndex],
|
|
102
|
+
query: e.target.value,
|
|
103
|
+
};
|
|
104
|
+
onChange({
|
|
105
|
+
...sqlEditorConfig,
|
|
106
|
+
queries: newQueries,
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
const handleRunQueryRef = useRef(handleRunQuery);
|
|
110
|
+
handleRunQueryRef.current = handleRunQuery;
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
const handleKeyDown = (evt) => {
|
|
113
|
+
if (evt instanceof KeyboardEvent &&
|
|
114
|
+
evt.key === 'Enter' &&
|
|
115
|
+
(evt.metaKey || evt.ctrlKey || evt.shiftKey)) {
|
|
116
|
+
void handleRunQueryRef.current();
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
globalThis.addEventListener('keydown', handleKeyDown);
|
|
120
|
+
return () => {
|
|
121
|
+
globalThis.removeEventListener('keydown', handleKeyDown);
|
|
122
|
+
};
|
|
123
|
+
}, []);
|
|
124
|
+
const handleExport = () => {
|
|
125
|
+
if (!results)
|
|
126
|
+
return;
|
|
127
|
+
const blob = new Blob([csvFormat(results.toArray())], {
|
|
128
|
+
type: 'text/plain;charset=utf-8',
|
|
129
|
+
});
|
|
130
|
+
saveAs(blob, `export-${genRandomStr(5)}.csv`);
|
|
131
|
+
};
|
|
132
|
+
const [createTableModalOpen, setCreateTableModalOpen] = useState(false);
|
|
133
|
+
const handleCreateTable = useCallback(() => {
|
|
134
|
+
setCreateTableModalOpen(true);
|
|
135
|
+
}, []);
|
|
136
|
+
const handleToggleDocs = useCallback(() => {
|
|
137
|
+
setShowDocs(!showDocs);
|
|
138
|
+
}, [showDocs]);
|
|
139
|
+
const currentQuery = sqlEditorConfig.queries[getCurrentQueryIndex()]?.query ?? DEFAULT_QUERY;
|
|
140
|
+
const [queryToDelete, setQueryToDelete] = useState(null);
|
|
141
|
+
const [queryToRename, setQueryToRename] = useState(null);
|
|
142
|
+
const handleStartRename = (queryId, currentName, event) => {
|
|
143
|
+
event.preventDefault();
|
|
144
|
+
setQueryToRename({ id: queryId, name: currentName });
|
|
145
|
+
};
|
|
146
|
+
const handleFinishRename = (newName) => {
|
|
147
|
+
if (queryToRename) {
|
|
148
|
+
const newQueries = sqlEditorConfig.queries.map((q) => q.id === queryToRename.id ? { ...q, name: newName || q.name } : q);
|
|
149
|
+
onChange({
|
|
150
|
+
...sqlEditorConfig,
|
|
151
|
+
queries: newQueries,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
setQueryToRename(null);
|
|
155
|
+
};
|
|
156
|
+
const handleDeleteQuery = (queryId, event) => {
|
|
157
|
+
event.stopPropagation();
|
|
158
|
+
const currentIndex = getQueryIndexById(queryId);
|
|
159
|
+
setQueryToDelete(queryId);
|
|
160
|
+
// Pre-select the previous query if we're deleting the current one
|
|
161
|
+
if (queryId === sqlEditorConfig.selectedQueryId && currentIndex > 0) {
|
|
162
|
+
const prevId = sqlEditorConfig.queries[currentIndex - 1]?.id;
|
|
163
|
+
if (prevId) {
|
|
164
|
+
onChange({
|
|
165
|
+
...sqlEditorConfig,
|
|
166
|
+
selectedQueryId: prevId,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
const handleNewQuery = () => {
|
|
172
|
+
const newQueries = [...sqlEditorConfig.queries];
|
|
173
|
+
const newQuery = {
|
|
174
|
+
id: genRandomStr(8),
|
|
175
|
+
name: generateUniqueName('Untitled', newQueries.map((q) => q.name)),
|
|
176
|
+
query: DEFAULT_QUERY,
|
|
177
|
+
};
|
|
178
|
+
newQueries.push(newQuery);
|
|
179
|
+
onChange({
|
|
180
|
+
...sqlEditorConfig,
|
|
181
|
+
queries: newQueries,
|
|
182
|
+
selectedQueryId: newQuery.id,
|
|
183
|
+
});
|
|
184
|
+
};
|
|
185
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "absolute right-12", children: _jsxs(Button, { size: "sm", variant: showDocs ? 'secondary' : 'outline', onClick: handleToggleDocs, children: [_jsx(BookOpenIcon, { className: "w-4 h-4 mr-2" }), "SQL reference"] }) }), _jsxs("div", { className: "flex flex-col w-full gap-2", children: [_jsx("div", { className: "flex items-center gap-2 ml-1 mr-10 mb-2", children: _jsx("h2", { className: "text-lg font-semibold", children: "SQL Editor" }) }), _jsx("div", { className: "flex-grow h-full bg-muted", children: _jsxs(ResizablePanelGroup, { direction: "horizontal", className: "h-full", children: [_jsx(ResizablePanel, { defaultSize: 20, children: tablesLoading ? (_jsx(SpinnerPane, { h: "100%" })) : tablesError ? (_jsxs("div", { className: "p-4 text-red-500", children: ["Error loading tables: ", tablesError.message] })) : (_jsx(TablesList, { schema: "information_schema", tableNames: tables, selectedTable: selectedTable, onSelect: handleSelectTable })) }), _jsx(ResizableHandle, { withHandle: true }), _jsx(ResizablePanel, { defaultSize: showDocs ? 50 : 80, children: _jsxs(ResizablePanelGroup, { direction: "vertical", className: "h-full", children: [_jsx(ResizablePanel, { defaultSize: 50, children: _jsx("div", { className: "flex flex-col h-full gap-2", children: _jsxs(Tabs, { value: sqlEditorConfig.selectedQueryId, onValueChange: handleTabChange, className: "flex flex-col flex-grow overflow-hidden", children: [_jsxs("div", { className: "flex items-center gap-2 border-b border-border", children: [_jsxs(Button, { size: "sm", onClick: () => void handleRunQuery(), className: "uppercase", children: [_jsx(PlayIcon, { className: "w-4 h-4 mr-2" }), "Run"] }), _jsx(TabsList, { className: "flex-1", children: sqlEditorConfig.queries.map((q) => (_jsxs("div", { className: "relative", children: [_jsx(TabsTrigger, { value: q.id, className: "min-w-[60px] px-6 pr-8", children: q.name }), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx("div", { className: "absolute right-0 top-1/2 -translate-y-1/2 h-6 w-6 flex items-center justify-center cursor-pointer hover:bg-accent rounded-sm", onClick: (e) => e.stopPropagation(), children: _jsx(MoreVerticalIcon, { className: "h-3 w-3" }) }) }), _jsxs(DropdownMenuContent, { children: [_jsx(DropdownMenuItem, { onClick: (e) => {
|
|
186
|
+
e.stopPropagation();
|
|
187
|
+
handleStartRename(q.id, q.name, e);
|
|
188
|
+
}, children: "Rename" }), sqlEditorConfig.queries.length > 1 && (_jsx(DropdownMenuItem, { onClick: (e) => {
|
|
189
|
+
e.stopPropagation();
|
|
190
|
+
handleDeleteQuery(q.id, e);
|
|
191
|
+
}, className: "text-red-500", children: "Delete" }))] })] })] }, q.id))) }), _jsx(Button, { size: "icon", variant: "ghost", onClick: handleNewQuery, className: "ml-2", children: _jsx(PlusIcon, { className: "h-4 w-4" }) })] }), sqlEditorConfig.queries.map((q) => (_jsx(TabsContent, { value: q.id, className: "flex-grow data-[state=active]:flex-grow", children: _jsx(Textarea, { id: q.id, value: q.query, onChange: handleUpdateQuery, className: "h-full font-mono text-sm resize-none bg-muted" }) }, q.id)))] }) }) }), _jsx(ResizableHandle, { withHandle: true }), _jsx(ResizablePanel, { defaultSize: 50, children: _jsx("div", { className: "h-full overflow-hidden bg-muted text-sm", children: loading ? (_jsx(SpinnerPane, { h: "100%" })) : selectedTable ? (_jsx(QueryDataTable, { query: `SELECT * FROM ${schema}.${escapeId(selectedTable)}` })) : error ? (_jsx("div", { className: "w-full h-full p-5 overflow-auto", children: _jsx("pre", { className: "text-xs leading-tight text-red-500", children: error }) })) : resultsTableData ? (_jsxs("div", { className: "flex-grow overflow-hidden flex flex-col relative", children: [_jsx(DataTableVirtualized, { ...resultsTableData }), _jsxs("div", { className: "absolute bottom-0 right-0 flex gap-2 p-2", children: [_jsxs(Button, { size: "sm", disabled: !resultsTableData, onClick: handleCreateTable, children: [_jsx(PlusIcon, { className: "w-4 h-4 mr-2" }), "Create table"] }), _jsxs(Button, { size: "sm", disabled: !results, onClick: handleExport, children: [_jsx(DownloadIcon, { className: "w-4 h-4 mr-2" }), "Export"] })] })] })) : null }) })] }) }), showDocs && (_jsxs(_Fragment, { children: [_jsx(ResizableHandle, { withHandle: true }), _jsx(ResizablePanel, { defaultSize: 30, children: documentationPanel })] }))] }) }), _jsx(CreateTableModal, { query: currentQuery, isOpen: createTableModalOpen, onClose: () => setCreateTableModalOpen(false), onAddOrUpdateSqlQuery: onAddOrUpdateSqlQuery }), _jsx(DeleteSqlQueryModal, { isOpen: queryToDelete !== null, onClose: () => setQueryToDelete(null), onConfirm: () => {
|
|
192
|
+
const newQueries = sqlEditorConfig.queries.filter((q) => q.id !== queryToDelete);
|
|
193
|
+
const deletedIndex = getQueryIndexById(queryToDelete);
|
|
194
|
+
const selectedQueryId = newQueries[Math.min(deletedIndex, newQueries.length - 1)]?.id ||
|
|
195
|
+
newQueries[0]?.id;
|
|
196
|
+
if (selectedQueryId) {
|
|
197
|
+
onChange({
|
|
198
|
+
...sqlEditorConfig,
|
|
199
|
+
queries: newQueries,
|
|
200
|
+
selectedQueryId,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
setQueryToDelete(null);
|
|
204
|
+
} }), _jsx(RenameSqlQueryModal, { isOpen: queryToRename !== null, onClose: () => setQueryToRename(null), initialName: queryToRename?.name ?? '', onRename: handleFinishRename })] })] }));
|
|
205
|
+
};
|
|
206
|
+
export default SqlEditor;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SqlEditorModal.d.ts","sourceRoot":"","sources":["../src/SqlEditorModal.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAiB,MAAM,OAAO,CAAC;AACtC,OAAkB,EAAC,KAAK,EAAC,MAAM,aAAa,CAAC;AAE7C,QAAA,MAAM,cAAc,EAAE,KAAK,CAAC,EAAE,CAAC,KAAK,CAenC,CAAC;AAEF,eAAe,cAAc,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { SpinnerPane } from '@sqlrooms/ui';
|
|
4
|
+
import { Dialog, DialogContent, DialogHeader, DialogOverlay, DialogTitle, } from '@sqlrooms/ui';
|
|
5
|
+
import { Suspense } from 'react';
|
|
6
|
+
import SqlEditor from './SqlEditor';
|
|
7
|
+
const SqlEditorModal = (props) => {
|
|
8
|
+
const { isOpen, onClose } = props;
|
|
9
|
+
return (_jsxs(Dialog, { open: isOpen, onOpenChange: (open) => !open && onClose(), children: [_jsx(DialogOverlay, { className: "bg-background/80" }), _jsxs(DialogContent, { className: "max-w-[100vw] max-h-[100vh] w-[100vw] h-[100vh] p-3", children: [_jsx(DialogHeader, { className: "sr-only", children: _jsx(DialogTitle, { children: "SQL Editor" }) }), _jsx(Suspense, { fallback: _jsx(SpinnerPane, { h: "100%" }), children: _jsx(SqlEditor, { ...props }) })] })] }));
|
|
10
|
+
};
|
|
11
|
+
export default SqlEditorModal;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { FC } from 'react';
|
|
2
|
+
type Props = {
|
|
3
|
+
schema: string;
|
|
4
|
+
tableNames: string[];
|
|
5
|
+
selectedTable?: string;
|
|
6
|
+
onSelect: (name: string) => void;
|
|
7
|
+
onChange?: () => void;
|
|
8
|
+
renderTableButton?: (tableName: string, onSelect: Props['onSelect']) => React.ReactNode;
|
|
9
|
+
};
|
|
10
|
+
declare const TablesList: FC<Props>;
|
|
11
|
+
export { TablesList };
|
|
12
|
+
//# sourceMappingURL=TablesList.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TablesList.d.ts","sourceRoot":"","sources":["../src/TablesList.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,OAAO,CAAC;AAE9B,KAAK,KAAK,GAAG;IACX,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,iBAAiB,CAAC,EAAE,CAClB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,KAAK,CAAC,UAAU,CAAC,KACxB,KAAK,CAAC,SAAS,CAAC;CACtB,CAAC;AAEF,QAAA,MAAM,UAAU,EAAE,EAAE,CAAC,KAAK,CA+BzB,CAAC;AAEF,OAAO,EAAC,UAAU,EAAC,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button } from '@sqlrooms/ui';
|
|
3
|
+
import { TableIcon } from 'lucide-react';
|
|
4
|
+
const TablesList = (props) => {
|
|
5
|
+
const { tableNames, selectedTable, onSelect, renderTableButton = (tableName, onSelect) => (_jsxs(Button, { className: "w-full justify-start font-normal overflow-hidden whitespace-normal min-h-[25px] text-sm text-left break-words select-text", variant: selectedTable === tableName ? 'secondary' : 'ghost', size: "sm", onClick: () => onSelect(tableName), children: [_jsx(TableIcon, { className: "h-4 w-4" }), tableName] })), } = props;
|
|
6
|
+
return (_jsx("div", { className: "h-full bg-background/10 px-2 py-4 overflow-auto", children: _jsx("ul", { className: "space-y-1", children: tableNames.map((tableName, i) => (_jsx("li", { children: _jsx("div", { className: "flex items-center gap-1", children: renderTableButton(tableName, onSelect) }) }, i))) }) }));
|
|
7
|
+
};
|
|
8
|
+
export { TablesList };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,OAAO,IAAI,gBAAgB,EAAC,MAAM,oBAAoB,CAAC;AAC/D,OAAO,EAAC,OAAO,IAAI,SAAS,EAAC,MAAM,aAAa,CAAC;AACjD,YAAY,EAAC,KAAK,EAAC,MAAM,aAAa,CAAC;AACvC,OAAO,EAAC,OAAO,IAAI,cAAc,EAAC,MAAM,kBAAkB,CAAC"}
|
package/dist/index.js
ADDED
package/eslint.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sqlrooms/sql-editor",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"types": "src/index.ts",
|
|
10
|
+
"module": "dist/index.js",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"scripts": {
|
|
13
|
+
"dev": "tsc -w",
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"lint": "eslint ."
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@hookform/resolvers": "^3.10.0",
|
|
19
|
+
"@sqlrooms/data-table": "0.0.0",
|
|
20
|
+
"@sqlrooms/duckdb": "0.0.0",
|
|
21
|
+
"@sqlrooms/layout": "0.0.0",
|
|
22
|
+
"@sqlrooms/project-config": "0.0.0",
|
|
23
|
+
"@sqlrooms/ui": "0.0.0",
|
|
24
|
+
"@sqlrooms/utils": "0.0.0",
|
|
25
|
+
"apache-arrow": "^14.0.2",
|
|
26
|
+
"d3-dsv": "^3.0.1",
|
|
27
|
+
"file-saver": "^2.0.5",
|
|
28
|
+
"lucide-react": "^0.323.0",
|
|
29
|
+
"react": "^18.2.0",
|
|
30
|
+
"react-dom": "^18.2.0",
|
|
31
|
+
"react-hook-form": "^7.54.2",
|
|
32
|
+
"zod": "^3.22.4"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/d3-dsv": "^3.0.7",
|
|
36
|
+
"@types/file-saver": "^2.0.7",
|
|
37
|
+
"@types/react": "^18.2.48",
|
|
38
|
+
"@types/react-dom": "^18.2.18"
|
|
39
|
+
},
|
|
40
|
+
"gitHead": "4b0c709542475e4f95db0b2a8405ecadcf2ec186"
|
|
41
|
+
}
|