@rebasepro/studio 0.24.0 → 0.24.1-canary.gcb918fc
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/dist/{ApiExplorer-CCtczj3F.js → ApiExplorer-Bzt7Ye49.js} +5 -3
- package/dist/ApiExplorer-Bzt7Ye49.js.map +1 -0
- package/dist/{ApiKeysView-B66TdzhH.js → ApiKeysView-Bqf6XYR1.js} +2 -1
- package/dist/{ApiKeysView-B66TdzhH.js.map → ApiKeysView-Bqf6XYR1.js.map} +1 -1
- package/dist/{BackupsView-DmSMzAbT.js → BackupsView-BhOHB6iP.js} +6 -20
- package/dist/BackupsView-BhOHB6iP.js.map +1 -0
- package/dist/{BranchesView-DP1a87E-.js → BranchesView-BHfwGTYc.js} +5 -3
- package/dist/BranchesView-BHfwGTYc.js.map +1 -0
- package/dist/{CronJobsView-TDErHTuK.js → CronJobsView-CMNK7eWO.js} +52 -59
- package/dist/CronJobsView-CMNK7eWO.js.map +1 -0
- package/dist/{JSEditor-B59pwg4c.js → JSEditor-DIO2Mw8q.js} +115 -96
- package/dist/JSEditor-DIO2Mw8q.js.map +1 -0
- package/dist/{LogsExplorer-BKM4yfP8.js → LogsExplorer-C-bwDFIl.js} +19 -23
- package/dist/LogsExplorer-C-bwDFIl.js.map +1 -0
- package/dist/{RLSEditor-BlLLV6xs.js → RLSEditor-jFD_d06f.js} +10 -8
- package/dist/RLSEditor-jFD_d06f.js.map +1 -0
- package/dist/{SQLEditor-1An7w0j6.js → SQLEditor-D5Ci4pyW.js} +372 -545
- package/dist/SQLEditor-D5Ci4pyW.js.map +1 -0
- package/dist/{SchemaVisualizer-DCLVfA5t.js → SchemaVisualizer-D-rAqwul.js} +479 -206
- package/dist/SchemaVisualizer-D-rAqwul.js.map +1 -0
- package/dist/{StorageView-CbsHuCBf.js → StorageView-CPkDKbsX.js} +498 -241
- package/dist/StorageView-CPkDKbsX.js.map +1 -0
- package/dist/assets/dagre-layout.worker-BFnhAzuR.js.map +1 -0
- package/dist/components/SQLEditor/SQLEditor.d.ts +11 -0
- package/dist/components/SchemaVisualizer/dagre-layout.d.ts +32 -0
- package/dist/components/SchemaVisualizer/dagre-layout.worker.d.ts +13 -0
- package/dist/components/SchemaVisualizer/layout-runner.d.ts +23 -0
- package/dist/components/SchemaVisualizer/schema-visualizer.utils.d.ts +21 -10
- package/dist/components/SchemaVisualizer/useSchemaGraph.d.ts +38 -1
- package/dist/components/StorageView/useVisibleFileMetadata.d.ts +46 -0
- package/dist/dagre-layout.worker-j9NbH9Gb.js +27 -0
- package/dist/dagre-layout.worker-j9NbH9Gb.js.map +1 -0
- package/dist/index.es.js +16 -14
- package/dist/index.es.js.map +1 -1
- package/dist/sql_utils-BZD31xE3.js +478 -0
- package/dist/sql_utils-BZD31xE3.js.map +1 -0
- package/dist/utils/sql_utils.d.ts +60 -0
- package/package.json +9 -9
- package/dist/ApiExplorer-CCtczj3F.js.map +0 -1
- package/dist/BackupsView-DmSMzAbT.js.map +0 -1
- package/dist/BranchesView-DP1a87E-.js.map +0 -1
- package/dist/CronJobsView-TDErHTuK.js.map +0 -1
- package/dist/JSEditor-B59pwg4c.js.map +0 -1
- package/dist/LogsExplorer-BKM4yfP8.js.map +0 -1
- package/dist/RLSEditor-BlLLV6xs.js.map +0 -1
- package/dist/SQLEditor-1An7w0j6.js.map +0 -1
- package/dist/SchemaVisualizer-DCLVfA5t.js.map +0 -1
- package/dist/StorageView-CbsHuCBf.js.map +0 -1
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
import { a as needsDestructiveConfirmation, c as rememberColumnWidth, d as resultsToCsv, i as keepLatestColumnWidths, l as resolveCellEdit, m as withAutoLimit, n as formatSql, o as quoteIdentifier, p as typedJsonRows, r as groupSchemaRows, s as quoteTableName, t as buildExplainSql, u as resolveQueryCollections } from "./sql_utils-BZD31xE3.js";
|
|
1
2
|
import { t as MonacoEditor } from "./MonacoEditor-CUxy5_k5.js";
|
|
2
|
-
import { ConfirmationDialog, ErrorView, IconForView, useRebaseContext, useSnackbarController, useStudioCollectionRegistry, useStudioSidePanelController, useTranslation } from "@rebasepro/app";
|
|
3
|
+
import { ConfirmationDialog, ErrorView, IconForView, useApiBase, useApiConfig, useRebaseContext, useSnackbarController, useStudioCollectionRegistry, useStudioSidePanelController, useTranslation } from "@rebasepro/app";
|
|
3
4
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
-
import { Alert, Button, Checkbox, CircularProgress, Collapse, CopyIcon, DatabaseIcon, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Menu, MenuIcon, MenuItem, MoreVerticalIcon, PencilIcon, PlayIcon, PlusIcon, RefreshCwIcon, ResizablePanels, Tab, Tabs, TerminalIcon, TextField, TextareaAutosize, Tooltip, Trash2Icon, Typography, VirtualTable, XIcon, cls, defaultBorderMixin, iconSize, isKeyHandled } from "@rebasepro/ui";
|
|
5
|
+
import { Alert, Button, Checkbox, Chip, CircularProgress, Collapse, CopyIcon, DatabaseIcon, Dialog, DialogActions, DialogContent, DialogTitle, HeartIcon, IconButton, Menu, MenuIcon, MenuItem, MoreVerticalIcon, PencilIcon, PlayIcon, PlusIcon, RefreshCwIcon, ResizablePanels, SquareIcon, Tab, Tabs, TerminalIcon, TextField, TextareaAutosize, Tooltip, Trash2Icon, Typography, VirtualTable, XIcon, cls, defaultBorderMixin, iconSize, isKeyHandled } from "@rebasepro/ui";
|
|
5
6
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
-
import { isArrayValue, isRecordValue, readStoredJson, readStoredString,
|
|
7
|
+
import { isArrayValue, isRecordValue, readStoredJson, readStoredString, writeStoredJson, writeStoredString } from "@rebasepro/utils";
|
|
7
8
|
import { createPortal } from "react-dom";
|
|
8
|
-
import { astVisitor, parse } from "pgsql-ast-parser";
|
|
9
9
|
//#region src/components/SQLEditor/SchemaBrowser.tsx
|
|
10
10
|
var SchemaBrowser = ({ onTableClick, schemas, isSchemaLoading, schemaError, onRetrySchema }) => {
|
|
11
11
|
const [expandedSchemas, setExpandedSchemas] = useState({ public: true });
|
|
@@ -31,9 +31,10 @@ var SchemaBrowser = ({ onTableClick, schemas, isSchemaLoading, schemaError, onRe
|
|
|
31
31
|
className: "font-semibold uppercase tracking-wider text-text-disabled dark:text-text-disabled-dark",
|
|
32
32
|
children: t("studio_schema_tables")
|
|
33
33
|
}), /* @__PURE__ */ jsx(IconButton, {
|
|
34
|
+
"aria-label": t("refresh"),
|
|
34
35
|
size: "small",
|
|
35
36
|
onClick: onRetrySchema,
|
|
36
|
-
title: "
|
|
37
|
+
title: t("refresh"),
|
|
37
38
|
children: /* @__PURE__ */ jsx(RefreshCwIcon, { size: iconSize.smallest })
|
|
38
39
|
})]
|
|
39
40
|
}), /* @__PURE__ */ jsx("div", {
|
|
@@ -99,19 +100,21 @@ var SchemaBrowser = ({ onTableClick, schemas, isSchemaLoading, schemaError, onRe
|
|
|
99
100
|
className: "flex opacity-0 group-hover:opacity-100 focus-within:opacity-100 absolute right-1 items-center bg-surface-raised px-1 gap-1 rounded transition-opacity",
|
|
100
101
|
children: [
|
|
101
102
|
/* @__PURE__ */ jsx(IconButton, {
|
|
103
|
+
"aria-label": t("copy_table_name"),
|
|
102
104
|
size: "small",
|
|
103
105
|
onClick: (e) => {
|
|
104
106
|
e.stopPropagation();
|
|
105
107
|
navigator.clipboard.writeText(table.tableName);
|
|
106
108
|
},
|
|
107
|
-
title: "
|
|
109
|
+
title: t("copy_table_name"),
|
|
108
110
|
children: /* @__PURE__ */ jsx(CopyIcon, { size: iconSize.small })
|
|
109
111
|
}),
|
|
110
112
|
/* @__PURE__ */ jsxs(Menu, {
|
|
111
113
|
trigger: /* @__PURE__ */ jsx(IconButton, {
|
|
114
|
+
"aria-label": t("generate_sql_templates"),
|
|
112
115
|
size: "small",
|
|
113
116
|
onClick: (e) => e.stopPropagation(),
|
|
114
|
-
title: "
|
|
117
|
+
title: t("generate_sql_templates"),
|
|
115
118
|
children: /* @__PURE__ */ jsx(MoreVerticalIcon, { size: iconSize.small })
|
|
116
119
|
}),
|
|
117
120
|
children: [
|
|
@@ -195,13 +198,14 @@ var SchemaBrowser = ({ onTableClick, schemas, isSchemaLoading, schemaError, onRe
|
|
|
195
198
|
children: col.dataType
|
|
196
199
|
}),
|
|
197
200
|
/* @__PURE__ */ jsx(IconButton, {
|
|
201
|
+
"aria-label": t("copy_column_name"),
|
|
198
202
|
size: "smallest",
|
|
199
203
|
className: "opacity-0 group-hover:opacity-100 absolute right-1 bg-surface-sheet transition-colors pointer-events-auto",
|
|
200
204
|
onClick: (e) => {
|
|
201
205
|
e.stopPropagation();
|
|
202
206
|
navigator.clipboard.writeText(col.name);
|
|
203
207
|
},
|
|
204
|
-
title: "
|
|
208
|
+
title: t("copy_column_name"),
|
|
205
209
|
children: /* @__PURE__ */ jsx(CopyIcon, { size: iconSize.smallest })
|
|
206
210
|
})
|
|
207
211
|
]
|
|
@@ -301,6 +305,7 @@ var SQLEditorSidebar = ({ onSelectSnippet, onTableClick, snippets, history, onDe
|
|
|
301
305
|
children: snippet.sql
|
|
302
306
|
}),
|
|
303
307
|
/* @__PURE__ */ jsx(IconButton, {
|
|
308
|
+
"aria-label": t("studio_sql_sidebar_delete_snippet"),
|
|
304
309
|
size: "smallest",
|
|
305
310
|
className: "absolute right-1 top-1 opacity-0 group-hover:opacity-100 text-text-disabled hover:text-red-500 transition-opacity",
|
|
306
311
|
onClick: (e) => {
|
|
@@ -333,6 +338,7 @@ var SQLEditorSidebar = ({ onSelectSnippet, onTableClick, snippets, history, onDe
|
|
|
333
338
|
children: snippet.sql
|
|
334
339
|
}),
|
|
335
340
|
/* @__PURE__ */ jsx(IconButton, {
|
|
341
|
+
"aria-label": t("studio_sql_sidebar_delete_snippet"),
|
|
336
342
|
size: "smallest",
|
|
337
343
|
className: "absolute right-1 top-1 opacity-0 group-hover:opacity-100 text-text-disabled hover:text-red-500 transition-opacity",
|
|
338
344
|
onClick: (e) => {
|
|
@@ -392,335 +398,6 @@ var SQLEditorSidebar = ({ onSelectSnippet, onTableClick, snippets, history, onDe
|
|
|
392
398
|
});
|
|
393
399
|
};
|
|
394
400
|
//#endregion
|
|
395
|
-
//#region src/utils/sql_utils.ts
|
|
396
|
-
/** The statements in `sqlText`, or `null` when the parser cannot read it. */
|
|
397
|
-
function parseStatements(sqlText) {
|
|
398
|
-
try {
|
|
399
|
-
return parse(sqlText);
|
|
400
|
-
} catch {
|
|
401
|
-
return null;
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
/**
|
|
405
|
-
* The `EXPLAIN` for one statement, or `null` when `sqlText` is not exactly one.
|
|
406
|
-
*
|
|
407
|
-
* Never `ANALYZE`: that executes the statement to time it, so explaining a
|
|
408
|
-
* `DELETE` deleted the rows — without the confirmation "Run" asks for.
|
|
409
|
-
*
|
|
410
|
-
* And never more than one statement. Sent as one simple query, `EXPLAIN (…)
|
|
411
|
-
* SELECT 1; DELETE …` explains the SELECT and *runs* the DELETE. Text the
|
|
412
|
-
* parser cannot read is still explained, as long as no `;` could be hiding a
|
|
413
|
-
* second statement in it.
|
|
414
|
-
*/
|
|
415
|
-
function buildExplainSql(sqlText) {
|
|
416
|
-
const statement = sqlText.trim().replace(/;+$/, "").trim();
|
|
417
|
-
if (!statement) return null;
|
|
418
|
-
const statements = parseStatements(statement);
|
|
419
|
-
return (statements ? statements.length === 1 : !statement.includes(";")) ? `EXPLAIN (FORMAT JSON) ${statement}` : null;
|
|
420
|
-
}
|
|
421
|
-
/**
|
|
422
|
-
* Whether the console's automatic `LIMIT` may be appended to `sqlText`: one
|
|
423
|
-
* top-level SELECT that has no limit of its own.
|
|
424
|
-
*
|
|
425
|
-
* Decided on the parsed statement, not the text. A search for the word SELECT
|
|
426
|
-
* also found it inside `INSERT INTO … SELECT` and `CREATE TABLE … AS SELECT`,
|
|
427
|
-
* which then copied a thousand rows and reported success, and at the start of
|
|
428
|
-
* a script whose last statement the appended `LIMIT` turned into a syntax
|
|
429
|
-
* error. Text the parser cannot read is left as written.
|
|
430
|
-
*/
|
|
431
|
-
function acceptsAutoLimit(sqlText) {
|
|
432
|
-
const statements = parseStatements(sqlText);
|
|
433
|
-
if (!statements || statements.length !== 1) return false;
|
|
434
|
-
const [statement] = statements;
|
|
435
|
-
return statement.type === "select" && !statement.limit;
|
|
436
|
-
}
|
|
437
|
-
/** A destructive command named anywhere in a text, comments included. */
|
|
438
|
-
var DESTRUCTIVE_COMMAND = /\b(DELETE|UPDATE|DROP|TRUNCATE)\b/i;
|
|
439
|
-
function isDestructiveStatement(statement) {
|
|
440
|
-
switch (statement.type) {
|
|
441
|
-
case "delete":
|
|
442
|
-
case "update": return !statement.where;
|
|
443
|
-
case "truncate table":
|
|
444
|
-
case "drop table":
|
|
445
|
-
case "drop sequence":
|
|
446
|
-
case "drop index":
|
|
447
|
-
case "drop type":
|
|
448
|
-
case "drop trigger":
|
|
449
|
-
case "drop function": return true;
|
|
450
|
-
case "alter table": return statement.changes.some((change) => change.type === "drop column" || change.type === "drop constraint");
|
|
451
|
-
case "with": return statement.bind.some((binding) => isDestructiveStatement(binding.statement)) || isDestructiveStatement(statement.in);
|
|
452
|
-
case "with recursive": return isDestructiveStatement(statement.in);
|
|
453
|
-
case "do": return DESTRUCTIVE_COMMAND.test(statement.code);
|
|
454
|
-
default: return false;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
/**
|
|
458
|
-
* Whether "Run" asks before running `sqlText`: a statement in it deletes or
|
|
459
|
-
* updates without a WHERE of its own, drops or truncates.
|
|
460
|
-
*
|
|
461
|
-
* Decided per parsed statement, not on the text. A search of the text for the
|
|
462
|
-
* word WHERE found it in another statement or in a comment, so `DELETE FROM
|
|
463
|
-
* posts; SELECT * FROM posts WHERE id = 1` deleted every post without asking.
|
|
464
|
-
* Text the parser cannot read is confirmed whenever it names a destructive
|
|
465
|
-
* command.
|
|
466
|
-
*/
|
|
467
|
-
function needsDestructiveConfirmation(sqlText) {
|
|
468
|
-
const statements = parseStatements(sqlText);
|
|
469
|
-
if (!statements) return DESTRUCTIVE_COMMAND.test(sqlText);
|
|
470
|
-
return statements.some(isDestructiveStatement);
|
|
471
|
-
}
|
|
472
|
-
/**
|
|
473
|
-
* A table as SQL: `"schema"."table"` when a schema is given, a bare `"table"`
|
|
474
|
-
* otherwise.
|
|
475
|
-
*/
|
|
476
|
-
function quoteTableName(tableName, schemaName) {
|
|
477
|
-
return schemaName ? `${quoteIdentifier(schemaName)}.${quoteIdentifier(tableName)}` : quoteIdentifier(tableName);
|
|
478
|
-
}
|
|
479
|
-
/** An identifier as SQL, whatever it holds: double-quoted, with `"` doubled. */
|
|
480
|
-
function quoteIdentifier(identifier) {
|
|
481
|
-
return `"${identifier.replace(/"/g, "\"\"")}"`;
|
|
482
|
-
}
|
|
483
|
-
var refuse = (key, params = {}) => ({ refusal: {
|
|
484
|
-
key,
|
|
485
|
-
params
|
|
486
|
-
} });
|
|
487
|
-
/**
|
|
488
|
-
* Which stored cell an edit of the result column `resultColumn` writes, and
|
|
489
|
-
* how its row is found again — or why no edit is safe.
|
|
490
|
-
*
|
|
491
|
-
* Decided on what the database reported, not on the query's text. Each
|
|
492
|
-
* column of the result says which table column it was read from, or that it
|
|
493
|
-
* was computed; a cell is editable only when it was read from a column of a
|
|
494
|
-
* table, every column of that table's primary key was read too, and each of
|
|
495
|
-
* them is the only column of the result with its name — a row holds one value
|
|
496
|
-
* per name.
|
|
497
|
-
*
|
|
498
|
-
* The text was the only evidence before, and it was wrong twice over. For
|
|
499
|
-
* `SELECT p.id, a.name FROM posts p JOIN authors a …` it took the post's `id`
|
|
500
|
-
* for the author's key and updated whichever author shared the post's id.
|
|
501
|
-
* For `SELECT id, lower(email) AS name FROM users` it wrote the edited text
|
|
502
|
-
* to `users.name`.
|
|
503
|
-
*
|
|
504
|
-
* The database reports a table, not which mention of it: in a self-join both
|
|
505
|
-
* sides are `authors`, and a key from one with a value from the other finds
|
|
506
|
-
* the wrong row. So the query is still read, for one thing — the table must
|
|
507
|
-
* be read once, and nothing it is read through may be read twice.
|
|
508
|
-
*/
|
|
509
|
-
function resolveCellEdit(sqlText, provenance, resultColumn) {
|
|
510
|
-
const sameName = provenance.columns.filter((column) => column.name === resultColumn);
|
|
511
|
-
if (sameName.length !== 1) return refuse("studio_sql_edit_duplicate_column", { column: resultColumn });
|
|
512
|
-
const source = sameName[0].source;
|
|
513
|
-
if (!source) return refuse("studio_sql_edit_computed_column", { column: resultColumn });
|
|
514
|
-
const tableLabel = `${source.schema}.${source.table}`;
|
|
515
|
-
const table = provenance.tables.find((t) => t.schema === source.schema && t.table === source.table);
|
|
516
|
-
if (!table || table.kind !== "table" && table.kind !== "partitioned table") return refuse("studio_sql_edit_not_a_table", {
|
|
517
|
-
column: resultColumn,
|
|
518
|
-
table: tableLabel
|
|
519
|
-
});
|
|
520
|
-
if (table.hasInheritors) return refuse("studio_sql_edit_inherited_table", { table: tableLabel });
|
|
521
|
-
if (table.primaryKey.length === 0) return refuse("studio_sql_edit_no_primary_key", { table: tableLabel });
|
|
522
|
-
const key = [];
|
|
523
|
-
for (const keyColumn of table.primaryKey) {
|
|
524
|
-
const holders = provenance.columns.filter((column) => column.source?.schema === source.schema && column.source.table === source.table && column.source.column === keyColumn);
|
|
525
|
-
const holder = holders.length === 1 && provenance.columns.filter((column) => column.name === holders[0].name).length === 1 ? holders[0] : void 0;
|
|
526
|
-
if (!holder) return refuse("studio_sql_edit_key_missing", {
|
|
527
|
-
table: tableLabel,
|
|
528
|
-
columns: table.primaryKey.join(", ")
|
|
529
|
-
});
|
|
530
|
-
key.push({
|
|
531
|
-
column: keyColumn,
|
|
532
|
-
resultColumn: holder.name
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
const readOnce = tableReadOnce(sqlText, source.table);
|
|
536
|
-
if (readOnce !== true) return readOnce;
|
|
537
|
-
return { target: {
|
|
538
|
-
schema: source.schema,
|
|
539
|
-
table: source.table,
|
|
540
|
-
column: source.column,
|
|
541
|
-
key
|
|
542
|
-
} };
|
|
543
|
-
}
|
|
544
|
-
/**
|
|
545
|
-
* Whether `sqlText` is one SELECT that reads `table` once — counting every
|
|
546
|
-
* mention of a table of that name, in any schema and at any depth, and every
|
|
547
|
-
* mention of a `WITH` query, since one read twice reads its tables twice.
|
|
548
|
-
*/
|
|
549
|
-
function tableReadOnce(sqlText, table) {
|
|
550
|
-
const statements = parseStatements(sqlText);
|
|
551
|
-
if (!statements) return refuse("studio_sql_edit_unreadable_query");
|
|
552
|
-
if (statements.length !== 1) return refuse("studio_sql_edit_not_a_select");
|
|
553
|
-
const [statement] = statements;
|
|
554
|
-
if (!(statement.type === "select" || statement.type === "with" && statement.in.type === "select" && statement.bind.every((binding) => binding.statement.type === "select"))) return refuse("studio_sql_edit_not_a_select");
|
|
555
|
-
const mentions = /* @__PURE__ */ new Map();
|
|
556
|
-
const withNames = [];
|
|
557
|
-
astVisitor((visitor) => ({
|
|
558
|
-
fromTable: (from) => {
|
|
559
|
-
const name = from.name.name.toLowerCase();
|
|
560
|
-
mentions.set(name, (mentions.get(name) ?? 0) + 1);
|
|
561
|
-
visitor.super().fromTable(from);
|
|
562
|
-
},
|
|
563
|
-
with: (withStatement) => {
|
|
564
|
-
withNames.push(...withStatement.bind.map((binding) => binding.alias.name.toLowerCase()));
|
|
565
|
-
visitor.super().with(withStatement);
|
|
566
|
-
}
|
|
567
|
-
})).statement(statement);
|
|
568
|
-
return (mentions.get(table.toLowerCase()) ?? 0) !== 1 || withNames.some((name) => (mentions.get(name) ?? 0) > 1) ? refuse("studio_sql_edit_table_read_twice", { table }) : true;
|
|
569
|
-
}
|
|
570
|
-
/**
|
|
571
|
-
* The collections a result's rows can be opened as: those whose table one of
|
|
572
|
-
* the result's columns was read from, with the table's (single-column) primary
|
|
573
|
-
* key read too, under a name no other column of the result has.
|
|
574
|
-
*
|
|
575
|
-
* Matched on the provenance the database reported. A text match took the
|
|
576
|
-
* result's `id` for every table of a join: `SELECT p.id, a.name FROM posts p
|
|
577
|
-
* JOIN authors a …` offered to open the author whose id was the post's.
|
|
578
|
-
*/
|
|
579
|
-
function resolveQueryCollections(provenance, collections) {
|
|
580
|
-
const results = [];
|
|
581
|
-
for (const table of provenance.tables) {
|
|
582
|
-
const collection = collections.find((c) => {
|
|
583
|
-
const tableName = ("table" in c ? c.table : void 0) || toSnakeCase(c.slug);
|
|
584
|
-
const schemaName = ("schema" in c ? c.schema : void 0) || "public";
|
|
585
|
-
return tableName === table.table && schemaName === table.schema;
|
|
586
|
-
});
|
|
587
|
-
if (!collection || table.primaryKey.length !== 1) continue;
|
|
588
|
-
const [keyColumn] = table.primaryKey;
|
|
589
|
-
const holders = provenance.columns.filter((column) => column.source?.schema === table.schema && column.source.table === table.table && column.source.column === keyColumn);
|
|
590
|
-
if (holders.length !== 1) continue;
|
|
591
|
-
const pkColumn = holders[0].name;
|
|
592
|
-
if (provenance.columns.filter((column) => column.name === pkColumn).length !== 1) continue;
|
|
593
|
-
results.push({
|
|
594
|
-
tableName: table.table,
|
|
595
|
-
schemaName: table.schema,
|
|
596
|
-
collection,
|
|
597
|
-
pkColumn
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
return results;
|
|
601
|
-
}
|
|
602
|
-
/** A character an unquoted identifier or keyword can hold, `$` included. */
|
|
603
|
-
var IDENTIFIER_CHARACTER = /[A-Za-z0-9_$]/;
|
|
604
|
-
/**
|
|
605
|
-
* Cut `sqlText` into code and the things in it whose text is data.
|
|
606
|
-
*
|
|
607
|
-
* A lexer, not a parser: it reads text the parser refuses as well, which is
|
|
608
|
-
* most of what a person types while writing a query.
|
|
609
|
-
*/
|
|
610
|
-
function splitSql(sqlText) {
|
|
611
|
-
const segments = [];
|
|
612
|
-
let code = "";
|
|
613
|
-
const push = (text, kind) => {
|
|
614
|
-
if (code) segments.push({
|
|
615
|
-
text: code,
|
|
616
|
-
kind: "code"
|
|
617
|
-
});
|
|
618
|
-
code = "";
|
|
619
|
-
segments.push({
|
|
620
|
-
text,
|
|
621
|
-
kind
|
|
622
|
-
});
|
|
623
|
-
};
|
|
624
|
-
let i = 0;
|
|
625
|
-
while (i < sqlText.length) {
|
|
626
|
-
const ch = sqlText[i];
|
|
627
|
-
const next = sqlText[i + 1];
|
|
628
|
-
if (ch === "-" && next === "-") {
|
|
629
|
-
const newline = sqlText.indexOf("\n", i);
|
|
630
|
-
const end = newline === -1 ? sqlText.length : newline + 1;
|
|
631
|
-
push(sqlText.slice(i, end), "line comment");
|
|
632
|
-
i = end;
|
|
633
|
-
continue;
|
|
634
|
-
}
|
|
635
|
-
if (ch === "/" && next === "*") {
|
|
636
|
-
let depth = 1;
|
|
637
|
-
let j = i + 2;
|
|
638
|
-
while (j < sqlText.length && depth > 0) if (sqlText[j] === "/" && sqlText[j + 1] === "*") {
|
|
639
|
-
depth++;
|
|
640
|
-
j += 2;
|
|
641
|
-
} else if (sqlText[j] === "*" && sqlText[j + 1] === "/") {
|
|
642
|
-
depth--;
|
|
643
|
-
j += 2;
|
|
644
|
-
} else j++;
|
|
645
|
-
push(sqlText.slice(i, j), "block comment");
|
|
646
|
-
i = j;
|
|
647
|
-
continue;
|
|
648
|
-
}
|
|
649
|
-
if (ch === "'" || ch === "\"") {
|
|
650
|
-
const backslashEscapes = ch === "'" && /(^|[^A-Za-z0-9_$])[eE]$/.test(code);
|
|
651
|
-
let j = i + 1;
|
|
652
|
-
while (j < sqlText.length) if (backslashEscapes && sqlText[j] === "\\") j += 2;
|
|
653
|
-
else if (sqlText[j] === ch && sqlText[j + 1] === ch) j += 2;
|
|
654
|
-
else if (sqlText[j] === ch) {
|
|
655
|
-
j++;
|
|
656
|
-
break;
|
|
657
|
-
} else j++;
|
|
658
|
-
push(sqlText.slice(i, j), ch === "'" ? "literal" : "identifier");
|
|
659
|
-
i = j;
|
|
660
|
-
continue;
|
|
661
|
-
}
|
|
662
|
-
if (ch === "$" && !IDENTIFIER_CHARACTER.test(code.slice(-1))) {
|
|
663
|
-
const tag = /^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/.exec(sqlText.slice(i));
|
|
664
|
-
if (tag) {
|
|
665
|
-
const close = sqlText.indexOf(tag[0], i + tag[0].length);
|
|
666
|
-
const end = close === -1 ? sqlText.length : close + tag[0].length;
|
|
667
|
-
push(sqlText.slice(i, end), "literal");
|
|
668
|
-
i = end;
|
|
669
|
-
continue;
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
code += ch;
|
|
673
|
-
i++;
|
|
674
|
-
}
|
|
675
|
-
if (code) segments.push({
|
|
676
|
-
text: code,
|
|
677
|
-
kind: "code"
|
|
678
|
-
});
|
|
679
|
-
return segments;
|
|
680
|
-
}
|
|
681
|
-
/**
|
|
682
|
-
* The console's "Format SQL": runs of whitespace become one space, a comma is
|
|
683
|
-
* followed by one, and an `=` standing alone gets one on each side — in the
|
|
684
|
-
* code only.
|
|
685
|
-
*
|
|
686
|
-
* A literal, a quoted identifier and a comment are copied as they are: their
|
|
687
|
-
* text is data, and rewriting `'a,b'` to `'a, b'` changes the value the next
|
|
688
|
-
* Run writes. A line comment keeps the line break that ends it, or it would
|
|
689
|
-
* swallow the code after it. `>=`, `<=`, `!=`, `:=` and `=>` are operators of
|
|
690
|
-
* their own and are left whole.
|
|
691
|
-
*/
|
|
692
|
-
function formatSql(sqlText) {
|
|
693
|
-
const segments = splitSql(sqlText);
|
|
694
|
-
return segments.map((segment, index) => {
|
|
695
|
-
if (segment.kind !== "code") return segment.text;
|
|
696
|
-
const tidied = segment.text.replace(/\s+/g, " ").replace(/\s*,\s*/g, ", ").replace(/(?<![<>!:=~*+\-/%^&|#@?])\s*=\s*(?![=>~*<])/g, " = ");
|
|
697
|
-
return segments[index - 1]?.kind === "line comment" ? tidied.trimStart() : tidied;
|
|
698
|
-
}).join("").trim();
|
|
699
|
-
}
|
|
700
|
-
/**
|
|
701
|
-
* `sqlText` with the console's automatic `LIMIT` on it, or `null` when it
|
|
702
|
-
* takes none (see {@link acceptsAutoLimit}).
|
|
703
|
-
*
|
|
704
|
-
* The limit goes straight after the statement's last token — before a
|
|
705
|
-
* trailing comment, which would otherwise swallow it, and in place of a
|
|
706
|
-
* trailing `;`. A query that computes an aggregate is limited like any other:
|
|
707
|
-
* the limit applies to the rows it returns, so a `count(*)` still counts every
|
|
708
|
-
* row, and a window `count(*) OVER ()` is no longer a way past the cap.
|
|
709
|
-
*/
|
|
710
|
-
function withAutoLimit(sqlText, limit) {
|
|
711
|
-
if (!acceptsAutoLimit(sqlText)) return null;
|
|
712
|
-
const segments = splitSql(sqlText);
|
|
713
|
-
const isComment = (segment) => segment.kind === "line comment" || segment.kind === "block comment";
|
|
714
|
-
const isFiller = (segment) => isComment(segment) || segment.kind === "code" && /^[\s;]*$/.test(segment.text);
|
|
715
|
-
let last = segments.length - 1;
|
|
716
|
-
while (last >= 0 && isFiller(segments[last])) last--;
|
|
717
|
-
if (last < 0) return null;
|
|
718
|
-
const lastSegment = segments[last];
|
|
719
|
-
const head = lastSegment.kind === "code" ? lastSegment.text.replace(/[\s;]+$/, "") : lastSegment.text;
|
|
720
|
-
const tail = (lastSegment.kind === "code" ? lastSegment.text.slice(head.length) : "").replace(/;/g, "") + segments.slice(last + 1).map((segment) => segment.kind === "code" ? segment.text.replace(/;/g, "") : segment.text).join("");
|
|
721
|
-
return `${segments.slice(0, last).map((segment) => segment.text).join("")}${head} LIMIT ${limit};${tail.trimEnd()}`;
|
|
722
|
-
}
|
|
723
|
-
//#endregion
|
|
724
401
|
//#region src/components/SQLEditor/ExplainVisualizer.tsx
|
|
725
402
|
var ExplainVisualizer = ({ plan, isRoot = true }) => {
|
|
726
403
|
const [expanded, setExpanded] = useState(true);
|
|
@@ -864,6 +541,11 @@ var ExplainVisualizer = ({ plan, isRoot = true }) => {
|
|
|
864
541
|
};
|
|
865
542
|
//#endregion
|
|
866
543
|
//#region src/components/SQLEditor/SQLEditor.tsx
|
|
544
|
+
/**
|
|
545
|
+
* The box a result cell is edited in. It closes once: Enter saves, Escape
|
|
546
|
+
* cancels, a click away saves — and the blur a closing box can still receive
|
|
547
|
+
* is not a second answer, which made Enter save twice and Escape save at all.
|
|
548
|
+
*/
|
|
867
549
|
var FixedEditorOverlay = ({ displayValue, onSave, onCancel }) => {
|
|
868
550
|
const [rect, setRect] = useState(null);
|
|
869
551
|
const [windowSize, setWindowSize] = useState({
|
|
@@ -871,6 +553,14 @@ var FixedEditorOverlay = ({ displayValue, onSave, onCancel }) => {
|
|
|
871
553
|
height: 1e3
|
|
872
554
|
});
|
|
873
555
|
const anchorRef = useRef(null);
|
|
556
|
+
const closedRef = useRef(false);
|
|
557
|
+
/** Close the box, saving `typed` when it is given. Only the first call counts. */
|
|
558
|
+
const close = (typed) => {
|
|
559
|
+
if (closedRef.current) return;
|
|
560
|
+
closedRef.current = true;
|
|
561
|
+
if (typed !== void 0) onSave(typed);
|
|
562
|
+
onCancel();
|
|
563
|
+
};
|
|
874
564
|
useEffect(() => {
|
|
875
565
|
if (anchorRef.current && anchorRef.current.parentElement) setRect(anchorRef.current.parentElement.getBoundingClientRect());
|
|
876
566
|
if (typeof window !== "undefined") {
|
|
@@ -923,17 +613,13 @@ var FixedEditorOverlay = ({ displayValue, onSave, onCancel }) => {
|
|
|
923
613
|
e.target.value = "";
|
|
924
614
|
e.target.value = val;
|
|
925
615
|
},
|
|
926
|
-
onBlur: (e) =>
|
|
927
|
-
onSave(e.target.value);
|
|
928
|
-
onCancel();
|
|
929
|
-
},
|
|
616
|
+
onBlur: (e) => close(e.target.value),
|
|
930
617
|
onKeyDown: (e) => {
|
|
931
618
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
932
619
|
e.preventDefault();
|
|
933
|
-
|
|
934
|
-
onCancel();
|
|
620
|
+
close(e.currentTarget.value);
|
|
935
621
|
}
|
|
936
|
-
if (e.key === "Escape")
|
|
622
|
+
if (e.key === "Escape") close();
|
|
937
623
|
}
|
|
938
624
|
})
|
|
939
625
|
}), document.body)
|
|
@@ -1000,7 +686,8 @@ var SQLEditor = () => {
|
|
|
1000
686
|
lastExecutedConnection: null,
|
|
1001
687
|
ranAs: null,
|
|
1002
688
|
lastProvenance: null,
|
|
1003
|
-
notices: []
|
|
689
|
+
notices: [],
|
|
690
|
+
truncatedFrom: null
|
|
1004
691
|
}));
|
|
1005
692
|
if (restored.length > 0) return restored;
|
|
1006
693
|
return [{
|
|
@@ -1017,7 +704,8 @@ var SQLEditor = () => {
|
|
|
1017
704
|
lastExecutedConnection: null,
|
|
1018
705
|
ranAs: null,
|
|
1019
706
|
lastProvenance: null,
|
|
1020
|
-
notices: []
|
|
707
|
+
notices: [],
|
|
708
|
+
truncatedFrom: null
|
|
1021
709
|
}];
|
|
1022
710
|
});
|
|
1023
711
|
const [activeTabId, setActiveTabId] = useState(() => {
|
|
@@ -1035,6 +723,12 @@ var SQLEditor = () => {
|
|
|
1035
723
|
const loading = activeTab.loading;
|
|
1036
724
|
const error = activeTab.error;
|
|
1037
725
|
const execTime = activeTab.execTime;
|
|
726
|
+
/**
|
|
727
|
+
* The SQL whose rows are on screen, which their column widths are kept
|
|
728
|
+
* under. Not the text being typed: keyed by that, every keystroke handed
|
|
729
|
+
* the grid other widths, and every visible cell was drawn again.
|
|
730
|
+
*/
|
|
731
|
+
const resultSql = activeTab.lastExecutedSql ?? "";
|
|
1038
732
|
const setSql = (newSql) => updateActiveTab({ sql: newSql });
|
|
1039
733
|
useEffect(() => {
|
|
1040
734
|
let mounted = true;
|
|
@@ -1178,30 +872,7 @@ var SQLEditor = () => {
|
|
|
1178
872
|
c.table_schema, c.table_name, c.ordinal_position;
|
|
1179
873
|
`, { database: selectedDatabase });
|
|
1180
874
|
const processGrouped = (data) => {
|
|
1181
|
-
|
|
1182
|
-
const schema = curr.schema || curr.SCHEMA || curr.table_schema || "public";
|
|
1183
|
-
const table = curr.table || curr.TABLE || curr.table_name;
|
|
1184
|
-
const column = curr.column || curr.COLUMN || curr.column_name;
|
|
1185
|
-
const dataType = curr.data_type || curr.DATA_TYPE || "";
|
|
1186
|
-
const isPrimaryKey = curr.is_pk === true || curr.is_pk === "true";
|
|
1187
|
-
if (!acc[schema]) acc[schema] = [];
|
|
1188
|
-
let tableInfo = acc[schema].find((t) => t.tableName === table);
|
|
1189
|
-
if (!tableInfo) {
|
|
1190
|
-
tableInfo = {
|
|
1191
|
-
schemaName: schema,
|
|
1192
|
-
tableName: table,
|
|
1193
|
-
columns: []
|
|
1194
|
-
};
|
|
1195
|
-
acc[schema].push(tableInfo);
|
|
1196
|
-
}
|
|
1197
|
-
tableInfo.columns.push({
|
|
1198
|
-
name: column,
|
|
1199
|
-
dataType,
|
|
1200
|
-
isPrimaryKey
|
|
1201
|
-
});
|
|
1202
|
-
return acc;
|
|
1203
|
-
}, {});
|
|
1204
|
-
setSchemas(grouped);
|
|
875
|
+
setSchemas(groupSchemaRows(data));
|
|
1205
876
|
};
|
|
1206
877
|
if (!result || !Array.isArray(result)) if (result && typeof result === "object" && "rows" in result && Array.isArray(result.rows)) processGrouped(result.rows);
|
|
1207
878
|
else {
|
|
@@ -1362,10 +1033,10 @@ var SQLEditor = () => {
|
|
|
1362
1033
|
t
|
|
1363
1034
|
]);
|
|
1364
1035
|
const [columnWidths, setColumnWidths] = useState(() => {
|
|
1365
|
-
return readStoredJson(`rebase_sql_column_widths_${client?.baseUrl ? client.baseUrl.replace(/^https?:\/\//, "").replace(/[^a-zA-Z0-9]/g, "_") : "default"}`, {
|
|
1036
|
+
return keepLatestColumnWidths(readStoredJson(`rebase_sql_column_widths_${client?.baseUrl ? client.baseUrl.replace(/^https?:\/\//, "").replace(/[^a-zA-Z0-9]/g, "_") : "default"}`, {
|
|
1366
1037
|
fallback: {},
|
|
1367
1038
|
accept: isRecordValue
|
|
1368
|
-
});
|
|
1039
|
+
}));
|
|
1369
1040
|
});
|
|
1370
1041
|
const [snippets, setSnippets] = useState([]);
|
|
1371
1042
|
const [history, setHistory] = useState([]);
|
|
@@ -1430,7 +1101,8 @@ var SQLEditor = () => {
|
|
|
1430
1101
|
lastExecutedConnection: null,
|
|
1431
1102
|
ranAs: null,
|
|
1432
1103
|
lastProvenance: null,
|
|
1433
|
-
notices: []
|
|
1104
|
+
notices: [],
|
|
1105
|
+
truncatedFrom: null
|
|
1434
1106
|
}]);
|
|
1435
1107
|
setActiveTabId(newId);
|
|
1436
1108
|
};
|
|
@@ -1447,22 +1119,46 @@ var SQLEditor = () => {
|
|
|
1447
1119
|
};
|
|
1448
1120
|
const handleColumnResize = useCallback(({ key, width }) => {
|
|
1449
1121
|
setColumnWidths((prev) => {
|
|
1450
|
-
const newWidths =
|
|
1451
|
-
...prev,
|
|
1452
|
-
[activeTab.sql]: {
|
|
1453
|
-
...prev[activeTab.sql] || {},
|
|
1454
|
-
[key]: width
|
|
1455
|
-
}
|
|
1456
|
-
};
|
|
1122
|
+
const newWidths = rememberColumnWidth(prev, resultSql, key, width);
|
|
1457
1123
|
writeStoredJson(`rebase_sql_column_widths_${projectPrefix}`, newWidths);
|
|
1458
1124
|
return newWidths;
|
|
1459
1125
|
});
|
|
1460
|
-
}, [
|
|
1126
|
+
}, [resultSql, projectPrefix]);
|
|
1461
1127
|
const handlePrettify = () => {
|
|
1462
1128
|
setSql(formatSql(activeTab.sql));
|
|
1463
1129
|
};
|
|
1464
1130
|
/** The editor, for the selection the toolbar's buttons act on. */
|
|
1465
1131
|
const editorHandle = useRef(null);
|
|
1132
|
+
/**
|
|
1133
|
+
* The tabs with a run or an explain in flight. A ref, not the tab's
|
|
1134
|
+
* `loading`: two Cmd+Enter in a row both read the render before the first
|
|
1135
|
+
* set it, and only the button was disabled — so an INSERT ran twice, and
|
|
1136
|
+
* the slower answer drew over the newer one.
|
|
1137
|
+
*/
|
|
1138
|
+
const tabsRunningRef = useRef(/* @__PURE__ */ new Set());
|
|
1139
|
+
/**
|
|
1140
|
+
* Each tab's run in flight, by tab, for "Cancel run": aborting it rejects
|
|
1141
|
+
* the run at once, and a server that can cancels the statement too.
|
|
1142
|
+
*/
|
|
1143
|
+
const runAbortRef = useRef(/* @__PURE__ */ new Map());
|
|
1144
|
+
const [cancellableTabs, setCancellableTabs] = useState(/* @__PURE__ */ new Set());
|
|
1145
|
+
const setCancellable = (tabId, cancellable) => setCancellableTabs((previous) => {
|
|
1146
|
+
const next = new Set(previous);
|
|
1147
|
+
if (cancellable) next.add(tabId);
|
|
1148
|
+
else next.delete(tabId);
|
|
1149
|
+
return next;
|
|
1150
|
+
});
|
|
1151
|
+
const handleCancelRun = () => runAbortRef.current.get(activeTab.id)?.abort();
|
|
1152
|
+
/** `work` on the active tab, unless that tab is already running something. */
|
|
1153
|
+
const runOnce = async (tabId, work) => {
|
|
1154
|
+
if (tabsRunningRef.current.has(tabId)) return;
|
|
1155
|
+
tabsRunningRef.current.add(tabId);
|
|
1156
|
+
try {
|
|
1157
|
+
await work();
|
|
1158
|
+
} finally {
|
|
1159
|
+
tabsRunningRef.current.delete(tabId);
|
|
1160
|
+
}
|
|
1161
|
+
};
|
|
1466
1162
|
const handleExplain = async () => {
|
|
1467
1163
|
const explainSql = buildExplainSql(editorHandle.current?.getSelectedText() ?? activeTab.sql);
|
|
1468
1164
|
if (!explainSql) {
|
|
@@ -1472,6 +1168,9 @@ var SQLEditor = () => {
|
|
|
1472
1168
|
});
|
|
1473
1169
|
return;
|
|
1474
1170
|
}
|
|
1171
|
+
await runOnce(activeTab.id, () => explain(explainSql));
|
|
1172
|
+
};
|
|
1173
|
+
const explain = async (explainSql) => {
|
|
1475
1174
|
updateActiveTab({
|
|
1476
1175
|
loading: true,
|
|
1477
1176
|
error: null,
|
|
@@ -1480,7 +1179,8 @@ var SQLEditor = () => {
|
|
|
1480
1179
|
lastExecutedConnection: null,
|
|
1481
1180
|
ranAs: null,
|
|
1482
1181
|
lastProvenance: null,
|
|
1483
|
-
notices: []
|
|
1182
|
+
notices: [],
|
|
1183
|
+
truncatedFrom: null
|
|
1484
1184
|
});
|
|
1485
1185
|
const start = performance.now();
|
|
1486
1186
|
try {
|
|
@@ -1501,23 +1201,33 @@ var SQLEditor = () => {
|
|
|
1501
1201
|
updateActiveTab({ loading: false });
|
|
1502
1202
|
}
|
|
1503
1203
|
};
|
|
1504
|
-
const executeRun = useCallback(async (
|
|
1204
|
+
const executeRun = useCallback((sqlOverride) => runOnce(activeTab.id, async () => {
|
|
1505
1205
|
const written = sqlOverride || activeTab.sql;
|
|
1506
1206
|
const sqlToRun = (autoLimit ? withAutoLimit(written, 1e3) : null) ?? written;
|
|
1507
1207
|
updateActiveTab({
|
|
1508
1208
|
loading: true,
|
|
1509
1209
|
error: null,
|
|
1510
1210
|
results: null,
|
|
1511
|
-
notices: []
|
|
1211
|
+
notices: [],
|
|
1212
|
+
truncatedFrom: null
|
|
1512
1213
|
});
|
|
1513
1214
|
const start = performance.now();
|
|
1215
|
+
const tabId = activeTab.id;
|
|
1216
|
+
const abort = databaseAdmin?.runSqlScript ? new AbortController() : void 0;
|
|
1217
|
+
if (abort) {
|
|
1218
|
+
runAbortRef.current.set(tabId, abort);
|
|
1219
|
+
setCancellable(tabId, true);
|
|
1220
|
+
}
|
|
1514
1221
|
try {
|
|
1515
1222
|
if (databaseAdmin?.runSqlScript || databaseAdmin?.executeSql) {
|
|
1516
1223
|
const connection = {
|
|
1517
1224
|
database: selectedDatabase,
|
|
1518
1225
|
role: selectedRole
|
|
1519
1226
|
};
|
|
1520
|
-
const run = databaseAdmin.runSqlScript ? await databaseAdmin.runSqlScript(sqlToRun,
|
|
1227
|
+
const run = databaseAdmin.runSqlScript ? await databaseAdmin.runSqlScript(sqlToRun, {
|
|
1228
|
+
...connection,
|
|
1229
|
+
signal: abort?.signal
|
|
1230
|
+
}) : void 0;
|
|
1521
1231
|
const rows = run ? run.rows : await databaseAdmin.executeSql(sqlToRun, connection);
|
|
1522
1232
|
updateActiveTab({
|
|
1523
1233
|
results: rows,
|
|
@@ -1532,18 +1242,24 @@ var SQLEditor = () => {
|
|
|
1532
1242
|
columns: Object.keys(rows[0] ?? {}).map((name) => ({ name })),
|
|
1533
1243
|
tables: []
|
|
1534
1244
|
},
|
|
1535
|
-
notices: run?.notices ?? []
|
|
1245
|
+
notices: run?.notices ?? [],
|
|
1246
|
+
truncatedFrom: run?.truncated ? run.rowCount ?? null : null
|
|
1536
1247
|
});
|
|
1537
1248
|
if (run?.effectiveRole && connection.role) setConnectionOwner(run.effectiveRole !== connection.role ? run.effectiveRole : null);
|
|
1538
1249
|
if (history[history.length - 1] !== activeTab.sql) saveHistory([...history, activeTab.sql]);
|
|
1539
1250
|
} else updateActiveTab({ error: t("studio_sql_execution_not_supported") });
|
|
1540
1251
|
} catch (e) {
|
|
1541
1252
|
const message = e instanceof Error ? e.message : String(e);
|
|
1542
|
-
updateActiveTab({ error: message || t("studio_sql_error_executing") });
|
|
1253
|
+
updateActiveTab({ error: abort?.signal.aborted ? t("studio_sql_run_cancelled") : message || t("studio_sql_error_executing") });
|
|
1543
1254
|
} finally {
|
|
1255
|
+
if (abort && runAbortRef.current.get(tabId) === abort) {
|
|
1256
|
+
runAbortRef.current.delete(tabId);
|
|
1257
|
+
setCancellable(tabId, false);
|
|
1258
|
+
}
|
|
1544
1259
|
updateActiveTab({ loading: false });
|
|
1545
1260
|
}
|
|
1546
|
-
}, [
|
|
1261
|
+
}), [
|
|
1262
|
+
activeTab.id,
|
|
1547
1263
|
activeTab.sql,
|
|
1548
1264
|
autoLimit,
|
|
1549
1265
|
databaseAdmin,
|
|
@@ -1595,11 +1311,7 @@ var SQLEditor = () => {
|
|
|
1595
1311
|
};
|
|
1596
1312
|
const handleExportCSV = () => {
|
|
1597
1313
|
if (!results || results.length === 0) return;
|
|
1598
|
-
const
|
|
1599
|
-
const str = String(val);
|
|
1600
|
-
return str.includes(",") ? `"${str}"` : str;
|
|
1601
|
-
}).join(","))].join("\n");
|
|
1602
|
-
const blob = new Blob([csv], { type: "text/csv" });
|
|
1314
|
+
const blob = new Blob([resultsToCsv(results)], { type: "text/csv" });
|
|
1603
1315
|
const url = window.URL.createObjectURL(blob);
|
|
1604
1316
|
const a = document.createElement("a");
|
|
1605
1317
|
a.href = url;
|
|
@@ -1607,9 +1319,71 @@ var SQLEditor = () => {
|
|
|
1607
1319
|
a.click();
|
|
1608
1320
|
window.URL.revokeObjectURL(url);
|
|
1609
1321
|
};
|
|
1322
|
+
const apiConfig = useApiConfig();
|
|
1323
|
+
const apiBase = useApiBase();
|
|
1324
|
+
const [downloadingFullResult, setDownloadingFullResult] = useState(false);
|
|
1325
|
+
/**
|
|
1326
|
+
* The whole result of the last run, past the rows the console keeps:
|
|
1327
|
+
* streamed by the server through a read-only cursor (`POST
|
|
1328
|
+
* /admin/sql/csv`), on the database and as the role the rows on screen
|
|
1329
|
+
* were read with.
|
|
1330
|
+
*/
|
|
1331
|
+
const handleDownloadFullCsv = async () => {
|
|
1332
|
+
const query = activeTab.lastExecutedSql;
|
|
1333
|
+
const connection = activeTab.lastExecutedConnection;
|
|
1334
|
+
if (!query || !apiBase) return;
|
|
1335
|
+
setDownloadingFullResult(true);
|
|
1336
|
+
try {
|
|
1337
|
+
const token = apiConfig?.getAuthToken ? await apiConfig.getAuthToken() : null;
|
|
1338
|
+
const response = await fetch(`${apiBase}/admin/sql/csv`, {
|
|
1339
|
+
method: "POST",
|
|
1340
|
+
headers: {
|
|
1341
|
+
"Content-Type": "application/json",
|
|
1342
|
+
...token ? { Authorization: `Bearer ${token}` } : {}
|
|
1343
|
+
},
|
|
1344
|
+
body: JSON.stringify({
|
|
1345
|
+
sql: query,
|
|
1346
|
+
database: connection?.database,
|
|
1347
|
+
role: connection?.role
|
|
1348
|
+
})
|
|
1349
|
+
});
|
|
1350
|
+
if (!response.ok) {
|
|
1351
|
+
const failure = await response.json().catch(() => null);
|
|
1352
|
+
const reason = typeof failure === "object" && failure !== null && "error" in failure && typeof failure.error === "object" && failure.error !== null && "message" in failure.error && typeof failure.error.message === "string" ? failure.error.message : `HTTP ${response.status}`;
|
|
1353
|
+
throw new Error(reason);
|
|
1354
|
+
}
|
|
1355
|
+
const blob = await response.blob();
|
|
1356
|
+
const url = window.URL.createObjectURL(blob);
|
|
1357
|
+
const a = document.createElement("a");
|
|
1358
|
+
a.href = url;
|
|
1359
|
+
a.download = `query_results_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19)}.csv`;
|
|
1360
|
+
a.click();
|
|
1361
|
+
window.URL.revokeObjectURL(url);
|
|
1362
|
+
} catch (e) {
|
|
1363
|
+
snackbarController.open({
|
|
1364
|
+
type: "error",
|
|
1365
|
+
message: t("studio_sql_download_full_failed", { message: e instanceof Error ? e.message : String(e) })
|
|
1366
|
+
});
|
|
1367
|
+
} finally {
|
|
1368
|
+
setDownloadingFullResult(false);
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1610
1371
|
const handleExportJSON = () => {
|
|
1611
1372
|
if (!results || results.length === 0) return;
|
|
1612
|
-
|
|
1373
|
+
downloadJson(results);
|
|
1374
|
+
};
|
|
1375
|
+
/**
|
|
1376
|
+
* The result's columns say their types when the script door ran it: each
|
|
1377
|
+
* value read by its type (numbers, booleans, json), where Export JSON
|
|
1378
|
+
* writes the database's text as the grid holds it.
|
|
1379
|
+
*/
|
|
1380
|
+
const typedColumns = activeTab.lastProvenance?.columns.some((column) => column.type !== void 0) ? activeTab.lastProvenance.columns : null;
|
|
1381
|
+
const handleExportTypedJSON = () => {
|
|
1382
|
+
if (!results || results.length === 0 || !typedColumns) return;
|
|
1383
|
+
downloadJson(typedJsonRows(results, typedColumns));
|
|
1384
|
+
};
|
|
1385
|
+
const downloadJson = (rows) => {
|
|
1386
|
+
const json = JSON.stringify(rows, null, 2);
|
|
1613
1387
|
const blob = new Blob([json], { type: "application/json" });
|
|
1614
1388
|
const url = window.URL.createObjectURL(blob);
|
|
1615
1389
|
const a = document.createElement("a");
|
|
@@ -1643,6 +1417,135 @@ var SQLEditor = () => {
|
|
|
1643
1417
|
});
|
|
1644
1418
|
});
|
|
1645
1419
|
};
|
|
1420
|
+
const savedWidths = columnWidths[resultSql];
|
|
1421
|
+
const resultColumnKeys = useMemo(() => results && results.length > 0 ? Object.keys(results[0]) : [], [results]);
|
|
1422
|
+
const readFromAppDatabase = activeTab.lastExecutedConnection !== null && (activeTab.lastExecutedConnection.database === void 0 || activeTab.lastExecutedConnection.database === appDatabase);
|
|
1423
|
+
const lastProvenance = activeTab.lastProvenance;
|
|
1424
|
+
const registryCollections = collectionRegistry.collections;
|
|
1425
|
+
const actionableCollections = useMemo(() => lastProvenance && registryCollections && readFromAppDatabase ? resolveQueryCollections(lastProvenance, registryCollections).filter((mc) => resultColumnKeys.includes(mc.pkColumn)) : [], [
|
|
1426
|
+
lastProvenance,
|
|
1427
|
+
registryCollections,
|
|
1428
|
+
readFromAppDatabase,
|
|
1429
|
+
resultColumnKeys
|
|
1430
|
+
]);
|
|
1431
|
+
const getRowEntityActions = useCallback((rowData) => {
|
|
1432
|
+
if (!rowData) return [];
|
|
1433
|
+
return actionableCollections.filter((mc) => rowData[mc.pkColumn] != null).map((mc) => ({
|
|
1434
|
+
collection: mc,
|
|
1435
|
+
entityId: rowData[mc.pkColumn]
|
|
1436
|
+
}));
|
|
1437
|
+
}, [actionableCollections]);
|
|
1438
|
+
const hasRowActions = actionableCollections.length > 0;
|
|
1439
|
+
const resultColumns = useMemo(() => {
|
|
1440
|
+
const dataColumns = resultColumnKeys.map((key) => ({
|
|
1441
|
+
key,
|
|
1442
|
+
title: key,
|
|
1443
|
+
width: savedWidths?.[key] ?? 150,
|
|
1444
|
+
sortable: false,
|
|
1445
|
+
resizable: true
|
|
1446
|
+
}));
|
|
1447
|
+
return hasRowActions ? [{
|
|
1448
|
+
key: "__cms_action__",
|
|
1449
|
+
title: "",
|
|
1450
|
+
width: 36,
|
|
1451
|
+
sortable: false,
|
|
1452
|
+
resizable: false
|
|
1453
|
+
}, ...dataColumns] : dataColumns;
|
|
1454
|
+
}, [
|
|
1455
|
+
resultColumnKeys,
|
|
1456
|
+
savedWidths,
|
|
1457
|
+
hasRowActions
|
|
1458
|
+
]);
|
|
1459
|
+
const renderResultCell = useCallback(({ rowData, column, rowIndex }) => {
|
|
1460
|
+
if (column.key === "__cms_action__") {
|
|
1461
|
+
const rowActions = getRowEntityActions(rowData ?? {});
|
|
1462
|
+
if (rowActions.length === 0) return /* @__PURE__ */ jsx("div", { className: "h-full w-full" });
|
|
1463
|
+
if (rowActions.length === 1) {
|
|
1464
|
+
const ra = rowActions[0];
|
|
1465
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1466
|
+
className: "h-full flex items-center justify-center",
|
|
1467
|
+
children: /* @__PURE__ */ jsx(Tooltip, {
|
|
1468
|
+
title: t("studio_sql_edit_entity", {
|
|
1469
|
+
name: ra.collection.collection.name,
|
|
1470
|
+
id: String(ra.entityId)
|
|
1471
|
+
}),
|
|
1472
|
+
children: /* @__PURE__ */ jsx(IconButton, {
|
|
1473
|
+
size: "small",
|
|
1474
|
+
"aria-label": t("studio_sql_edit_entity", {
|
|
1475
|
+
name: ra.collection.collection.name,
|
|
1476
|
+
id: String(ra.entityId)
|
|
1477
|
+
}),
|
|
1478
|
+
className: "text-surface-400 dark:text-surface-500 hover:text-surface-600 dark:hover:text-surface-300 transition-colors",
|
|
1479
|
+
onClick: (e) => {
|
|
1480
|
+
e.stopPropagation();
|
|
1481
|
+
sidePanelController?.open({
|
|
1482
|
+
path: ra.collection.collection.slug,
|
|
1483
|
+
entityId: ra.entityId,
|
|
1484
|
+
collection: ra.collection.collection,
|
|
1485
|
+
updateUrl: false
|
|
1486
|
+
});
|
|
1487
|
+
},
|
|
1488
|
+
children: /* @__PURE__ */ jsx(PencilIcon, { size: iconSize.smallest })
|
|
1489
|
+
})
|
|
1490
|
+
})
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1494
|
+
className: "h-full flex items-center justify-center",
|
|
1495
|
+
children: /* @__PURE__ */ jsx(Menu, {
|
|
1496
|
+
trigger: /* @__PURE__ */ jsx(IconButton, {
|
|
1497
|
+
"aria-label": t("more_actions"),
|
|
1498
|
+
size: "small",
|
|
1499
|
+
className: "text-surface-400 dark:text-surface-500 hover:text-surface-600 dark:hover:text-surface-300 transition-colors",
|
|
1500
|
+
onClick: (e) => e.stopPropagation(),
|
|
1501
|
+
children: /* @__PURE__ */ jsx(MoreVerticalIcon, { size: iconSize.smallest })
|
|
1502
|
+
}),
|
|
1503
|
+
children: rowActions.map((ra) => /* @__PURE__ */ jsx(MenuItem, {
|
|
1504
|
+
dense: true,
|
|
1505
|
+
onClick: () => {
|
|
1506
|
+
sidePanelController?.open({
|
|
1507
|
+
path: ra.collection.collection.slug,
|
|
1508
|
+
entityId: ra.entityId,
|
|
1509
|
+
collection: ra.collection.collection,
|
|
1510
|
+
updateUrl: false
|
|
1511
|
+
});
|
|
1512
|
+
},
|
|
1513
|
+
children: t("studio_sql_edit_entity", {
|
|
1514
|
+
name: ra.collection.collection.name,
|
|
1515
|
+
id: String(ra.entityId)
|
|
1516
|
+
})
|
|
1517
|
+
}, ra.collection.tableName))
|
|
1518
|
+
})
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
const isEditing = editingCell?.rowIndex === rowIndex && editingCell?.columnKey === column.key;
|
|
1522
|
+
const value = rowData ? rowData[column.key] : null;
|
|
1523
|
+
const displayValue = typeof value === "object" && value !== null ? JSON.stringify(value) : String(value ?? "");
|
|
1524
|
+
if (isEditing) return /* @__PURE__ */ jsx(FixedEditorOverlay, {
|
|
1525
|
+
displayValue,
|
|
1526
|
+
onSave: (val) => handleCellSave(val, rowData ?? {}, column.key, rowIndex),
|
|
1527
|
+
onCancel: () => setEditingCell(null)
|
|
1528
|
+
});
|
|
1529
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1530
|
+
className: "px-4 py-1.5 h-full flex items-center whitespace-nowrap text-[13px] text-text-primary dark:text-text-primary-dark font-mono cursor-text group/cell",
|
|
1531
|
+
onDoubleClick: () => handleDoubleClick(rowIndex, column.key, displayValue, rowData ?? {}),
|
|
1532
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1533
|
+
className: "truncate flex-grow",
|
|
1534
|
+
title: displayValue,
|
|
1535
|
+
children: value === null || value === void 0 ? /* @__PURE__ */ jsx("span", {
|
|
1536
|
+
className: "text-text-disabled dark:text-text-disabled-dark italic text-[11px]",
|
|
1537
|
+
children: "NULL"
|
|
1538
|
+
}) : displayValue
|
|
1539
|
+
})
|
|
1540
|
+
});
|
|
1541
|
+
}, [
|
|
1542
|
+
getRowEntityActions,
|
|
1543
|
+
editingCell,
|
|
1544
|
+
handleCellSave,
|
|
1545
|
+
handleDoubleClick,
|
|
1546
|
+
sidePanelController,
|
|
1547
|
+
t
|
|
1548
|
+
]);
|
|
1646
1549
|
const renderResults = () => {
|
|
1647
1550
|
if (loading) return /* @__PURE__ */ jsx("div", {
|
|
1648
1551
|
className: "flex-grow flex items-center justify-center",
|
|
@@ -1666,17 +1569,10 @@ var SQLEditor = () => {
|
|
|
1666
1569
|
className: "flex-grow flex items-center justify-center text-text-disabled dark:text-text-disabled-dark",
|
|
1667
1570
|
children: /* @__PURE__ */ jsxs("div", {
|
|
1668
1571
|
className: "text-center",
|
|
1669
|
-
children: [/* @__PURE__ */ jsx(
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
viewBox: "0 0 24 24",
|
|
1674
|
-
children: /* @__PURE__ */ jsx("path", {
|
|
1675
|
-
strokeLinecap: "round",
|
|
1676
|
-
strokeLinejoin: "round",
|
|
1677
|
-
strokeWidth: 1,
|
|
1678
|
-
d: "M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"
|
|
1679
|
-
})
|
|
1572
|
+
children: [/* @__PURE__ */ jsx(DatabaseIcon, {
|
|
1573
|
+
size: 48,
|
|
1574
|
+
strokeWidth: 1,
|
|
1575
|
+
className: "mx-auto mb-4 opacity-50"
|
|
1680
1576
|
}), /* @__PURE__ */ jsx(Typography, {
|
|
1681
1577
|
variant: "body2",
|
|
1682
1578
|
children: t("studio_sql_run_query_placeholder")
|
|
@@ -1688,8 +1584,9 @@ var SQLEditor = () => {
|
|
|
1688
1584
|
if (plan) return /* @__PURE__ */ jsxs("div", {
|
|
1689
1585
|
className: "flex-grow overflow-auto p-4 bg-surface-sheet flex flex-col items-start",
|
|
1690
1586
|
children: [/* @__PURE__ */ jsx(Typography, {
|
|
1691
|
-
variant: "
|
|
1692
|
-
|
|
1587
|
+
variant: "label",
|
|
1588
|
+
color: "secondary",
|
|
1589
|
+
className: "mb-4",
|
|
1693
1590
|
children: t("studio_sql_visual_execution_plan")
|
|
1694
1591
|
}), /* @__PURE__ */ jsx("div", {
|
|
1695
1592
|
className: "pb-12",
|
|
@@ -1711,31 +1608,6 @@ var SQLEditor = () => {
|
|
|
1711
1608
|
children: t("studio_sql_no_results")
|
|
1712
1609
|
})]
|
|
1713
1610
|
});
|
|
1714
|
-
const savedWidths = columnWidths[activeTab.sql] || {};
|
|
1715
|
-
const resultColumnKeys = Object.keys(results[0]);
|
|
1716
|
-
const readFromAppDatabase = activeTab.lastExecutedConnection !== null && (activeTab.lastExecutedConnection.database === void 0 || activeTab.lastExecutedConnection.database === appDatabase);
|
|
1717
|
-
const actionableCollections = activeTab.lastProvenance && collectionRegistry.collections && readFromAppDatabase ? resolveQueryCollections(activeTab.lastProvenance, collectionRegistry.collections).filter((mc) => resultColumnKeys.includes(mc.pkColumn)) : [];
|
|
1718
|
-
const getRowEntityActions = (rowData) => {
|
|
1719
|
-
if (!rowData) return [];
|
|
1720
|
-
return actionableCollections.filter((mc) => rowData[mc.pkColumn] != null).map((mc) => ({
|
|
1721
|
-
collection: mc,
|
|
1722
|
-
entityId: rowData[mc.pkColumn]
|
|
1723
|
-
}));
|
|
1724
|
-
};
|
|
1725
|
-
const dataColumns = resultColumnKeys.map((key) => ({
|
|
1726
|
-
key,
|
|
1727
|
-
title: key,
|
|
1728
|
-
width: savedWidths[key] ?? 150,
|
|
1729
|
-
sortable: false,
|
|
1730
|
-
resizable: true
|
|
1731
|
-
}));
|
|
1732
|
-
const columns = actionableCollections.length > 0 ? [{
|
|
1733
|
-
key: "__cms_action__",
|
|
1734
|
-
title: "",
|
|
1735
|
-
width: 36,
|
|
1736
|
-
sortable: false,
|
|
1737
|
-
resizable: false
|
|
1738
|
-
}, ...dataColumns] : dataColumns;
|
|
1739
1611
|
return /* @__PURE__ */ jsxs("div", {
|
|
1740
1612
|
className: "flex-grow flex flex-col overflow-hidden min-h-0",
|
|
1741
1613
|
children: [
|
|
@@ -1745,19 +1617,22 @@ var SQLEditor = () => {
|
|
|
1745
1617
|
title: t("studio_sql_admin_collections_tooltip"),
|
|
1746
1618
|
children: /* @__PURE__ */ jsx(Typography, {
|
|
1747
1619
|
variant: "caption",
|
|
1748
|
-
|
|
1620
|
+
color: "secondary",
|
|
1621
|
+
className: "mr-1 shrink-0 cursor-help",
|
|
1749
1622
|
children: t("studio_sql_collections_label")
|
|
1750
1623
|
})
|
|
1751
1624
|
}), /* @__PURE__ */ jsx("div", {
|
|
1752
1625
|
className: "flex items-center gap-1.5 overflow-x-auto no-scrollbar",
|
|
1753
1626
|
children: actionableCollections.map((mc) => /* @__PURE__ */ jsx(Tooltip, {
|
|
1754
1627
|
title: `Table "${mc.tableName}" → ${mc.collection.name} (PK: ${mc.pkColumn})`,
|
|
1755
|
-
children: /* @__PURE__ */
|
|
1756
|
-
|
|
1757
|
-
|
|
1628
|
+
children: /* @__PURE__ */ jsx(Chip, {
|
|
1629
|
+
size: "smallest",
|
|
1630
|
+
className: "whitespace-nowrap",
|
|
1631
|
+
icon: typeof mc.collection.icon === "string" ? /* @__PURE__ */ jsx(IconForView, {
|
|
1758
1632
|
collectionOrView: mc.collection,
|
|
1759
|
-
|
|
1760
|
-
}),
|
|
1633
|
+
size: "smallest"
|
|
1634
|
+
}) : void 0,
|
|
1635
|
+
children: mc.collection.name
|
|
1761
1636
|
})
|
|
1762
1637
|
}, mc.tableName))
|
|
1763
1638
|
})]
|
|
@@ -1766,93 +1641,12 @@ var SQLEditor = () => {
|
|
|
1766
1641
|
className: "flex-grow relative h-full min-h-0 min-w-0",
|
|
1767
1642
|
children: /* @__PURE__ */ jsx(VirtualTable, {
|
|
1768
1643
|
data: results,
|
|
1769
|
-
columns,
|
|
1644
|
+
columns: resultColumns,
|
|
1770
1645
|
rowHeight: 32,
|
|
1771
1646
|
headerHeight: 32,
|
|
1772
1647
|
extraData: editingCell,
|
|
1773
1648
|
onColumnResizeEnd: handleColumnResize,
|
|
1774
|
-
cellRenderer:
|
|
1775
|
-
if (column.key === "__cms_action__") {
|
|
1776
|
-
const rowActions = getRowEntityActions(rowData ?? {});
|
|
1777
|
-
if (rowActions.length === 0) return /* @__PURE__ */ jsx("div", { className: "h-full w-full" });
|
|
1778
|
-
if (rowActions.length === 1) {
|
|
1779
|
-
const ra = rowActions[0];
|
|
1780
|
-
return /* @__PURE__ */ jsx("div", {
|
|
1781
|
-
className: "h-full flex items-center justify-center",
|
|
1782
|
-
children: /* @__PURE__ */ jsx(Tooltip, {
|
|
1783
|
-
title: t("studio_sql_edit_entity", {
|
|
1784
|
-
name: ra.collection.collection.name,
|
|
1785
|
-
id: String(ra.entityId)
|
|
1786
|
-
}),
|
|
1787
|
-
children: /* @__PURE__ */ jsx(IconButton, {
|
|
1788
|
-
size: "small",
|
|
1789
|
-
"aria-label": t("studio_sql_edit_entity", {
|
|
1790
|
-
name: ra.collection.collection.name,
|
|
1791
|
-
id: String(ra.entityId)
|
|
1792
|
-
}),
|
|
1793
|
-
className: "text-surface-400 dark:text-surface-500 hover:text-surface-600 dark:hover:text-surface-300 transition-colors",
|
|
1794
|
-
onClick: (e) => {
|
|
1795
|
-
e.stopPropagation();
|
|
1796
|
-
sidePanelController?.open({
|
|
1797
|
-
path: ra.collection.collection.slug,
|
|
1798
|
-
entityId: ra.entityId,
|
|
1799
|
-
collection: ra.collection.collection,
|
|
1800
|
-
updateUrl: false
|
|
1801
|
-
});
|
|
1802
|
-
},
|
|
1803
|
-
children: /* @__PURE__ */ jsx(PencilIcon, { size: iconSize.smallest })
|
|
1804
|
-
})
|
|
1805
|
-
})
|
|
1806
|
-
});
|
|
1807
|
-
}
|
|
1808
|
-
return /* @__PURE__ */ jsx("div", {
|
|
1809
|
-
className: "h-full flex items-center justify-center",
|
|
1810
|
-
children: /* @__PURE__ */ jsx(Menu, {
|
|
1811
|
-
trigger: /* @__PURE__ */ jsx(IconButton, {
|
|
1812
|
-
size: "small",
|
|
1813
|
-
className: "text-surface-400 dark:text-surface-500 hover:text-surface-600 dark:hover:text-surface-300 transition-colors",
|
|
1814
|
-
onClick: (e) => e.stopPropagation(),
|
|
1815
|
-
children: /* @__PURE__ */ jsx(MoreVerticalIcon, { size: iconSize.smallest })
|
|
1816
|
-
}),
|
|
1817
|
-
children: rowActions.map((ra) => /* @__PURE__ */ jsx(MenuItem, {
|
|
1818
|
-
dense: true,
|
|
1819
|
-
onClick: () => {
|
|
1820
|
-
sidePanelController?.open({
|
|
1821
|
-
path: ra.collection.collection.slug,
|
|
1822
|
-
entityId: ra.entityId,
|
|
1823
|
-
collection: ra.collection.collection,
|
|
1824
|
-
updateUrl: false
|
|
1825
|
-
});
|
|
1826
|
-
},
|
|
1827
|
-
children: t("studio_sql_edit_entity", {
|
|
1828
|
-
name: ra.collection.collection.name,
|
|
1829
|
-
id: String(ra.entityId)
|
|
1830
|
-
})
|
|
1831
|
-
}, ra.collection.tableName))
|
|
1832
|
-
})
|
|
1833
|
-
});
|
|
1834
|
-
}
|
|
1835
|
-
const isEditing = editingCell?.rowIndex === rowIndex && editingCell?.columnKey === column.key;
|
|
1836
|
-
const value = rowData ? rowData[column.key] : null;
|
|
1837
|
-
const displayValue = typeof value === "object" && value !== null ? JSON.stringify(value) : String(value ?? "");
|
|
1838
|
-
if (isEditing) return /* @__PURE__ */ jsx(FixedEditorOverlay, {
|
|
1839
|
-
displayValue,
|
|
1840
|
-
onSave: (val) => handleCellSave(val, rowData ?? {}, column.key, rowIndex),
|
|
1841
|
-
onCancel: () => setEditingCell(null)
|
|
1842
|
-
});
|
|
1843
|
-
return /* @__PURE__ */ jsx("div", {
|
|
1844
|
-
className: "px-4 py-1.5 h-full flex items-center whitespace-nowrap text-[13px] text-text-primary dark:text-text-primary-dark font-mono cursor-text group/cell",
|
|
1845
|
-
onDoubleClick: () => handleDoubleClick(rowIndex, column.key, displayValue, rowData ?? {}),
|
|
1846
|
-
children: /* @__PURE__ */ jsx("div", {
|
|
1847
|
-
className: "truncate flex-grow",
|
|
1848
|
-
title: displayValue,
|
|
1849
|
-
children: value === null || value === void 0 ? /* @__PURE__ */ jsx("span", {
|
|
1850
|
-
className: "text-text-disabled dark:text-text-disabled-dark italic text-[11px]",
|
|
1851
|
-
children: "NULL"
|
|
1852
|
-
}) : displayValue
|
|
1853
|
-
})
|
|
1854
|
-
});
|
|
1855
|
-
}
|
|
1649
|
+
cellRenderer: renderResultCell
|
|
1856
1650
|
})
|
|
1857
1651
|
}),
|
|
1858
1652
|
/* @__PURE__ */ jsxs("div", {
|
|
@@ -1860,21 +1654,25 @@ var SQLEditor = () => {
|
|
|
1860
1654
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
1861
1655
|
className: "flex space-x-4",
|
|
1862
1656
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
1863
|
-
className: "flex items-center
|
|
1864
|
-
children: [/* @__PURE__ */ jsx(
|
|
1865
|
-
|
|
1657
|
+
className: "flex items-center gap-2",
|
|
1658
|
+
children: [/* @__PURE__ */ jsx(Typography, {
|
|
1659
|
+
variant: "caption",
|
|
1660
|
+
color: "secondary",
|
|
1866
1661
|
children: t("studio_sql_rows")
|
|
1867
|
-
}), /* @__PURE__ */ jsx(
|
|
1868
|
-
|
|
1662
|
+
}), /* @__PURE__ */ jsx(Typography, {
|
|
1663
|
+
variant: "mono",
|
|
1664
|
+
className: "text-[11px]",
|
|
1869
1665
|
children: results.length
|
|
1870
1666
|
})]
|
|
1871
1667
|
}), /* @__PURE__ */ jsxs("div", {
|
|
1872
|
-
className: "flex items-center
|
|
1873
|
-
children: [/* @__PURE__ */ jsx(
|
|
1874
|
-
|
|
1668
|
+
className: "flex items-center gap-2",
|
|
1669
|
+
children: [/* @__PURE__ */ jsx(Typography, {
|
|
1670
|
+
variant: "caption",
|
|
1671
|
+
color: "secondary",
|
|
1875
1672
|
children: t("studio_sql_time")
|
|
1876
|
-
}), /* @__PURE__ */ jsxs(
|
|
1877
|
-
|
|
1673
|
+
}), /* @__PURE__ */ jsxs(Typography, {
|
|
1674
|
+
variant: "mono",
|
|
1675
|
+
className: "text-[11px]",
|
|
1878
1676
|
children: [execTime, "ms"]
|
|
1879
1677
|
})]
|
|
1880
1678
|
})]
|
|
@@ -1884,21 +1682,28 @@ var SQLEditor = () => {
|
|
|
1884
1682
|
/* @__PURE__ */ jsx(Button, {
|
|
1885
1683
|
size: "small",
|
|
1886
1684
|
variant: "text",
|
|
1887
|
-
className: "
|
|
1685
|
+
className: "whitespace-nowrap",
|
|
1888
1686
|
onClick: handleExportMarkdown,
|
|
1889
1687
|
children: t("studio_sql_copy_markdown")
|
|
1890
1688
|
}),
|
|
1891
1689
|
/* @__PURE__ */ jsx(Button, {
|
|
1892
1690
|
size: "small",
|
|
1893
1691
|
variant: "text",
|
|
1894
|
-
className: "
|
|
1692
|
+
className: "whitespace-nowrap",
|
|
1895
1693
|
onClick: handleExportJSON,
|
|
1896
1694
|
children: t("studio_sql_export_json")
|
|
1897
1695
|
}),
|
|
1696
|
+
typedColumns && /* @__PURE__ */ jsx(Button, {
|
|
1697
|
+
size: "small",
|
|
1698
|
+
variant: "text",
|
|
1699
|
+
className: "whitespace-nowrap",
|
|
1700
|
+
onClick: handleExportTypedJSON,
|
|
1701
|
+
children: t("studio_sql_export_json_typed")
|
|
1702
|
+
}),
|
|
1898
1703
|
/* @__PURE__ */ jsx(Button, {
|
|
1899
1704
|
size: "small",
|
|
1900
1705
|
variant: "text",
|
|
1901
|
-
className: "
|
|
1706
|
+
className: "whitespace-nowrap",
|
|
1902
1707
|
onClick: handleExportCSV,
|
|
1903
1708
|
children: t("studio_sql_export_csv")
|
|
1904
1709
|
})
|
|
@@ -1927,6 +1732,7 @@ var SQLEditor = () => {
|
|
|
1927
1732
|
className: "flex h-full w-full bg-surface-card overflow-hidden text-text-primary dark:text-text-primary-dark",
|
|
1928
1733
|
children: [
|
|
1929
1734
|
/* @__PURE__ */ jsx(ResizablePanels, {
|
|
1735
|
+
dividerAriaLabel: t("resize_sidebar"),
|
|
1930
1736
|
orientation: "horizontal",
|
|
1931
1737
|
panelSizePercent: sidebarSize,
|
|
1932
1738
|
onPanelSizeChange: setSidebarSize,
|
|
@@ -2046,17 +1852,10 @@ var SQLEditor = () => {
|
|
|
2046
1852
|
isFavorite: !s.isFavorite
|
|
2047
1853
|
} : s));
|
|
2048
1854
|
},
|
|
2049
|
-
children: /* @__PURE__ */ jsx(
|
|
2050
|
-
|
|
1855
|
+
children: /* @__PURE__ */ jsx(HeartIcon, {
|
|
1856
|
+
size: iconSize.smallest,
|
|
2051
1857
|
fill: isFavorite ? "currentColor" : "none",
|
|
2052
|
-
|
|
2053
|
-
strokeWidth: 2,
|
|
2054
|
-
viewBox: "0 0 24 24",
|
|
2055
|
-
children: /* @__PURE__ */ jsx("path", {
|
|
2056
|
-
strokeLinecap: "round",
|
|
2057
|
-
strokeLinejoin: "round",
|
|
2058
|
-
d: "M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
|
|
2059
|
-
})
|
|
1858
|
+
className: cls(isFavorite ? "text-red-500" : "text-text-disabled dark:text-text-disabled-dark hover:text-text-primary")
|
|
2060
1859
|
})
|
|
2061
1860
|
})
|
|
2062
1861
|
}),
|
|
@@ -2136,10 +1935,20 @@ var SQLEditor = () => {
|
|
|
2136
1935
|
size: iconSize.smallest,
|
|
2137
1936
|
className: "mr-2"
|
|
2138
1937
|
}), t("studio_sql_run")]
|
|
1938
|
+
}),
|
|
1939
|
+
loading && cancellableTabs.has(activeTab.id) && /* @__PURE__ */ jsxs(Button, {
|
|
1940
|
+
onClick: handleCancelRun,
|
|
1941
|
+
size: "small",
|
|
1942
|
+
variant: "text",
|
|
1943
|
+
children: [/* @__PURE__ */ jsx(SquareIcon, {
|
|
1944
|
+
size: iconSize.smallest,
|
|
1945
|
+
className: "mr-2"
|
|
1946
|
+
}), t("studio_sql_cancel_run")]
|
|
2139
1947
|
})
|
|
2140
1948
|
]
|
|
2141
1949
|
})]
|
|
2142
1950
|
}), /* @__PURE__ */ jsx(ResizablePanels, {
|
|
1951
|
+
dividerAriaLabel: t("resize_editor"),
|
|
2143
1952
|
orientation: "vertical",
|
|
2144
1953
|
panelSizePercent: editorHeight,
|
|
2145
1954
|
onPanelSizeChange: setEditorHeight,
|
|
@@ -2176,6 +1985,24 @@ var SQLEditor = () => {
|
|
|
2176
1985
|
})
|
|
2177
1986
|
})
|
|
2178
1987
|
}),
|
|
1988
|
+
!loading && results && activeTab.truncatedFrom !== null && /* @__PURE__ */ jsx("div", {
|
|
1989
|
+
className: "px-4 pt-2 shrink-0",
|
|
1990
|
+
children: /* @__PURE__ */ jsx(Alert, {
|
|
1991
|
+
color: "info",
|
|
1992
|
+
size: "small",
|
|
1993
|
+
action: activeTab.lastExecutedSql && apiBase ? /* @__PURE__ */ jsx(Button, {
|
|
1994
|
+
size: "small",
|
|
1995
|
+
variant: "text",
|
|
1996
|
+
disabled: downloadingFullResult,
|
|
1997
|
+
onClick: handleDownloadFullCsv,
|
|
1998
|
+
children: downloadingFullResult ? t("studio_sql_downloading_full_csv") : t("studio_sql_download_full_csv")
|
|
1999
|
+
}) : void 0,
|
|
2000
|
+
children: t("studio_sql_result_truncated", {
|
|
2001
|
+
shown: results.length.toLocaleString(),
|
|
2002
|
+
total: activeTab.truncatedFrom.toLocaleString()
|
|
2003
|
+
})
|
|
2004
|
+
})
|
|
2005
|
+
}),
|
|
2179
2006
|
!loading && activeTab.notices.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
2180
2007
|
className: "px-4 pt-2 flex flex-col gap-1 shrink-0",
|
|
2181
2008
|
children: activeTab.notices.map((notice, index) => /* @__PURE__ */ jsx(Alert, {
|
|
@@ -2246,4 +2073,4 @@ var SQLEditor = () => {
|
|
|
2246
2073
|
//#endregion
|
|
2247
2074
|
export { SQLEditor };
|
|
2248
2075
|
|
|
2249
|
-
//# sourceMappingURL=SQLEditor-
|
|
2076
|
+
//# sourceMappingURL=SQLEditor-D5Ci4pyW.js.map
|