sqlite-hub 1.1.1 → 1.2.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/README.md +17 -196
- package/bin/sqlite-hub.js +7 -2
- package/frontend/js/api.js +60 -0
- package/frontend/js/app.js +201 -5
- package/frontend/js/components/dropdownButton.js +92 -0
- package/frontend/js/components/modal.js +991 -876
- package/frontend/js/components/queryEditor.js +24 -15
- package/frontend/js/components/queryHistoryDetail.js +20 -1
- package/frontend/js/components/queryHistoryHeader.js +3 -2
- package/frontend/js/components/queryResults.js +1 -1
- package/frontend/js/components/rowEditorPanel.js +53 -13
- package/frontend/js/components/sidebar.js +1 -0
- package/frontend/js/components/topNav.js +1 -4
- package/frontend/js/components/workspaceOpenDropdown.js +52 -0
- package/frontend/js/router.js +2 -0
- package/frontend/js/store.js +450 -38
- package/frontend/js/utils/emailPreview.js +28 -0
- package/frontend/js/utils/markdownDocuments.js +17 -1
- package/frontend/js/utils/riskySql.js +165 -0
- package/frontend/js/views/backups.js +204 -0
- package/frontend/js/views/charts.js +1 -1
- package/frontend/js/views/data.js +42 -16
- package/frontend/js/views/documents.js +184 -154
- package/frontend/js/views/settings.js +1 -0
- package/frontend/js/views/structure.js +47 -33
- package/frontend/js/views/tableDesigner.js +23 -0
- package/frontend/styles/components.css +133 -0
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/tokens.css +1 -0
- package/frontend/styles/views.css +33 -21
- package/package.json +2 -1
- package/server/routes/backups.js +120 -0
- package/server/routes/connections.js +26 -3
- package/server/routes/externalApi.js +6 -2
- package/server/server.js +3 -1
- package/server/services/databaseCommandService.js +4 -3
- package/server/services/sqlite/backupService.js +497 -66
- package/server/services/sqlite/connectionManager.js +2 -2
- package/server/services/sqlite/importService.js +25 -0
- package/server/services/sqlite/sqlExecutor.js +2 -0
- package/server/services/storage/appStateStore.js +379 -88
- package/tests/api-token-auth.test.js +45 -2
- package/tests/backup-manager.test.js +140 -0
- package/tests/backups-view.test.js +70 -0
- package/tests/charts-height-preset-storage.test.js +60 -0
- package/tests/charts-route-state.test.js +144 -0
- package/tests/cli-service-delegation.test.js +97 -0
- package/tests/connection-removal.test.js +52 -0
- package/tests/database-command-service.test.js +37 -2
- package/tests/documents-view.test.js +132 -0
- package/tests/dropdown-button.test.js +101 -0
- package/tests/email-preview.test.js +89 -0
- package/tests/markdown-documents.test.js +24 -0
- package/tests/query-editor.test.js +28 -0
- package/tests/query-history-detail.test.js +37 -0
- package/tests/query-history-header.test.js +30 -0
- package/tests/risky-sql.test.js +30 -0
- package/tests/row-editor-null-values.test.js +53 -0
- package/tests/settings-view.test.js +1 -0
- package/tests/structure-view.test.js +60 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { renderDropdownButton } from "./dropdownButton.js";
|
|
1
2
|
import { escapeHtml, highlightSql } from "../utils/format.js";
|
|
2
3
|
|
|
3
4
|
function renderLineNumbers(query) {
|
|
@@ -57,21 +58,29 @@ export function renderQueryEditor({
|
|
|
57
58
|
`;
|
|
58
59
|
|
|
59
60
|
const center = `
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
61
|
+
${renderDropdownButton({
|
|
62
|
+
icon: "edit_note",
|
|
63
|
+
label: "Editor",
|
|
64
|
+
title: "Editor actions",
|
|
65
|
+
items: [
|
|
66
|
+
{
|
|
67
|
+
action: "format-current-query",
|
|
68
|
+
icon: "format_align_left",
|
|
69
|
+
label: "Format",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
action: "clear-query",
|
|
73
|
+
icon: "delete_sweep",
|
|
74
|
+
label: "Clear",
|
|
75
|
+
danger: true,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
action: "copy-current-query",
|
|
79
|
+
icon: "content_copy",
|
|
80
|
+
label: "Copy to Clipboard",
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
})}
|
|
75
84
|
<button
|
|
76
85
|
class="${secondaryButtonClass}"
|
|
77
86
|
data-action="open-query-export-modal"
|
|
@@ -35,11 +35,30 @@ function getQueryTypeTone(queryType) {
|
|
|
35
35
|
return "muted";
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
function getExecutionSourceTone(source) {
|
|
39
|
+
if (source === "api") {
|
|
40
|
+
return "primary";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (source === "cli") {
|
|
44
|
+
return "warning";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (source === "mcp") {
|
|
48
|
+
return "muted";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return "success";
|
|
52
|
+
}
|
|
53
|
+
|
|
38
54
|
function renderRunItem(run) {
|
|
55
|
+
const executedBy = String(run.executedBy ?? "user").trim().toLowerCase() || "user";
|
|
56
|
+
|
|
39
57
|
return `
|
|
40
58
|
<div class="border border-outline-variant/10 bg-surface-container px-3 py-3">
|
|
41
59
|
<div class="flex flex-wrap items-center gap-2">
|
|
42
60
|
${renderStatusBadge(run.status, run.status === "error" ? "alert" : "success")}
|
|
61
|
+
${renderStatusBadge(executedBy, getExecutionSourceTone(executedBy))}
|
|
43
62
|
<span class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
44
63
|
${escapeHtml(formatCompactDateTime(run.executedAt))}
|
|
45
64
|
</span>
|
|
@@ -199,7 +218,7 @@ export function renderQueryHistoryDetail({
|
|
|
199
218
|
"</button>",
|
|
200
219
|
'<button class="delete-button" data-action="open-delete-query-history-modal" data-history-id="',
|
|
201
220
|
escapeHtml(item.id),
|
|
202
|
-
'" type="button">Delete</button></div>',
|
|
221
|
+
'" type="button"><span class="material-symbols-outlined text-sm">delete</span>Delete</button></div>',
|
|
203
222
|
'<div class="mt-6"><div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">SQL</div>',
|
|
204
223
|
'<pre class="query-history-detail-sql custom-scrollbar mt-2 overflow-auto border border-outline-variant/10 bg-surface-container-lowest p-4 font-mono text-sm leading-6 text-on-surface"><code>',
|
|
205
224
|
highlightSql(item.rawSql),
|
|
@@ -34,10 +34,11 @@ export function renderQueryHistorySearch({
|
|
|
34
34
|
placeholder = 'Search SQL, titles, notes...',
|
|
35
35
|
} = {}) {
|
|
36
36
|
return `
|
|
37
|
-
<label class="
|
|
37
|
+
<label class="query-history-search">
|
|
38
38
|
<span class="sr-only">Search query history</span>
|
|
39
|
+
<span class="material-symbols-outlined query-history-search__icon" aria-hidden="true">search</span>
|
|
39
40
|
<input
|
|
40
|
-
class="control-input w-full border border-outline-variant/20 bg-surface-container text-sm text-on-surface outline-none placeholder:text-on-surface-variant/35 focus:border-primary-container"
|
|
41
|
+
class="control-input query-history-search__input w-full border border-outline-variant/20 bg-surface-container text-sm text-on-surface outline-none placeholder:text-on-surface-variant/35 focus:border-primary-container"
|
|
41
42
|
data-bind="${escapeHtml(bind)}"
|
|
42
43
|
placeholder="${escapeHtml(placeholder)}"
|
|
43
44
|
type="search"
|
|
@@ -147,7 +147,7 @@ export function renderQueryResultsPane(
|
|
|
147
147
|
? renderDataGrid({
|
|
148
148
|
columns,
|
|
149
149
|
rows: result.rows ?? [],
|
|
150
|
-
tableClass: "min-w-full border-collapse text-left font-mono text-xs",
|
|
150
|
+
tableClass: "data-table min-w-full border-collapse text-left font-mono text-xs",
|
|
151
151
|
theadClass: "sticky top-0 z-10 bg-surface-container-highest text-on-surface",
|
|
152
152
|
tbodyClass: "divide-y divide-outline-variant/5",
|
|
153
153
|
getRowClass: (_, index) =>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { escapeHtml } from "../utils/format.js";
|
|
2
|
+
import { renderDropdownButton } from "./dropdownButton.js";
|
|
3
|
+
import { detectEmailValue } from "../utils/emailPreview.js";
|
|
2
4
|
import {
|
|
3
5
|
compactPathForDisplay,
|
|
4
6
|
detectFilePathValue,
|
|
@@ -49,6 +51,19 @@ function withUrlBadge(badges = [], url) {
|
|
|
49
51
|
return hasUrlBadge ? badges : [...badges, { label: "URL", tone: "url" }];
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
function withEmailBadge(badges = [], email) {
|
|
55
|
+
if (!email) {
|
|
56
|
+
return badges;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const hasEmailBadge = badges.some((badge) => {
|
|
60
|
+
const label = typeof badge === "object" ? badge.label : badge;
|
|
61
|
+
return String(label ?? "").toUpperCase() === "EMAIL";
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return hasEmailBadge ? badges : [...badges, { label: "EMAIL", tone: "email" }];
|
|
65
|
+
}
|
|
66
|
+
|
|
52
67
|
function getAllowedValues(field) {
|
|
53
68
|
const seen = new Set();
|
|
54
69
|
|
|
@@ -277,7 +292,11 @@ function renderReadonlyField(field = {}, tableMeta = {}) {
|
|
|
277
292
|
const protectedKeyColumn = isProtectedKeyColumn(columnName, tableMeta);
|
|
278
293
|
const filePathPreview = getFieldFilePathPreview({ ...field, rawValue }, tableMeta);
|
|
279
294
|
const url = getUrlValue(value);
|
|
280
|
-
const
|
|
295
|
+
const email = detectEmailValue(rawValue);
|
|
296
|
+
const badges = withEmailBadge(
|
|
297
|
+
withFilePathBadge(withUrlBadge(Array.isArray(label?.badges) ? label.badges : [], url), filePathPreview),
|
|
298
|
+
email
|
|
299
|
+
);
|
|
281
300
|
const displayLabel = typeof label === "object" ? label.label : label;
|
|
282
301
|
const jsonPreview = formatJsonPreview(value);
|
|
283
302
|
const valueState = getRowEditorValueState(rawValue);
|
|
@@ -290,7 +309,9 @@ function renderReadonlyField(field = {}, tableMeta = {}) {
|
|
|
290
309
|
data-row-editor-protected-key="${protectedKeyColumn ? "true" : "false"}"
|
|
291
310
|
${
|
|
292
311
|
url ? "data-row-editor-url-field" : ""
|
|
293
|
-
}
|
|
312
|
+
}
|
|
313
|
+
${email ? "data-row-editor-email-field" : ""}
|
|
314
|
+
>
|
|
294
315
|
<div class="flex flex-wrap items-center gap-2 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
295
316
|
<span>${escapeHtml(displayLabel)}</span>
|
|
296
317
|
${renderFieldBadgesWithCharacterCount(badges, rawValue)}
|
|
@@ -334,10 +355,11 @@ function renderEditableField(field, tableMeta = {}) {
|
|
|
334
355
|
const columnName = getFieldColumnName(field);
|
|
335
356
|
const protectedKeyColumn = isProtectedKeyColumn(columnName, tableMeta);
|
|
336
357
|
const url = getUrlValue(field.value);
|
|
358
|
+
const email = detectEmailValue(getFieldRawValue(field));
|
|
337
359
|
const allowedValues = getAllowedValues(field);
|
|
338
360
|
const filePathPreview = getFieldFilePathPreview(field, tableMeta);
|
|
339
361
|
const baseBadges = withCheckBadge(Array.isArray(field.badges) ? field.badges : [], allowedValues);
|
|
340
|
-
const badges = withFilePathBadge(withUrlBadge(baseBadges, url), filePathPreview);
|
|
362
|
+
const badges = withEmailBadge(withFilePathBadge(withUrlBadge(baseBadges, url), filePathPreview), email);
|
|
341
363
|
const jsonPreview = formatJsonPreview(field.value);
|
|
342
364
|
const inputType = field.inputType === "number" ? "number" : "text";
|
|
343
365
|
const numberStep = field.numberStep === "1" ? "1" : "any";
|
|
@@ -351,6 +373,7 @@ function renderEditableField(field, tableMeta = {}) {
|
|
|
351
373
|
data-row-editor-initial-state="${escapeHtml(valueState)}"
|
|
352
374
|
data-row-editor-protected-key="${protectedKeyColumn ? "true" : "false"}"
|
|
353
375
|
${url ? "data-row-editor-url-field" : ""}
|
|
376
|
+
${email ? "data-row-editor-email-field" : ""}
|
|
354
377
|
>
|
|
355
378
|
<span class="flex flex-wrap items-center gap-2 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
356
379
|
<span>${escapeHtml(field.label ?? field.name)}</span>
|
|
@@ -427,6 +450,10 @@ function getFieldBadgeClassName(tone) {
|
|
|
427
450
|
return "border-primary-container/35 bg-primary-container/15 text-primary-container";
|
|
428
451
|
}
|
|
429
452
|
|
|
453
|
+
if (tone === "email") {
|
|
454
|
+
return "border-tertiary-fixed-dim/35 bg-tertiary-fixed-dim/15 text-tertiary-fixed-dim";
|
|
455
|
+
}
|
|
456
|
+
|
|
430
457
|
if (tone === "check") {
|
|
431
458
|
return "border-tertiary-fixed-dim/35 bg-tertiary-fixed-dim/15 text-tertiary-fixed-dim";
|
|
432
459
|
}
|
|
@@ -488,16 +515,28 @@ export function renderRowEditorPanel({
|
|
|
488
515
|
].join("")
|
|
489
516
|
: "",
|
|
490
517
|
jsonActionsEnabled
|
|
491
|
-
?
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
"
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
518
|
+
? renderDropdownButton({
|
|
519
|
+
icon: "download",
|
|
520
|
+
label: "Export",
|
|
521
|
+
title: "Export row",
|
|
522
|
+
items: [
|
|
523
|
+
{
|
|
524
|
+
action: "copy-row-editor-json",
|
|
525
|
+
icon: "content_copy",
|
|
526
|
+
label: "Copy to clipboard",
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
action: "export-row-editor-json",
|
|
530
|
+
icon: "download",
|
|
531
|
+
label: "JSON file",
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
action: "insert-row-editor-json-into-document",
|
|
535
|
+
icon: "description",
|
|
536
|
+
label: "Markdown document",
|
|
537
|
+
},
|
|
538
|
+
],
|
|
539
|
+
})
|
|
501
540
|
: "",
|
|
502
541
|
canSubmit
|
|
503
542
|
? [
|
|
@@ -521,6 +560,7 @@ export function renderRowEditorPanel({
|
|
|
521
560
|
' type="button" ',
|
|
522
561
|
saving || deleting ? "disabled" : "",
|
|
523
562
|
">",
|
|
563
|
+
'<span class="material-symbols-outlined text-sm">delete</span>',
|
|
524
564
|
escapeHtml(deleting ? "Deleting..." : deleteLabel),
|
|
525
565
|
"</button>",
|
|
526
566
|
].join("")
|
|
@@ -19,6 +19,7 @@ const sidebarItems = [
|
|
|
19
19
|
{ label: 'TAGGING_QUEUE', href: '#/media-tagging/queue', key: 'mediaTaggingQueue' },
|
|
20
20
|
],
|
|
21
21
|
},
|
|
22
|
+
{ label: 'Backups', href: '#/backups', key: 'backups', icon: 'inventory_2' },
|
|
22
23
|
{ label: 'Settings', href: '#/settings', key: 'settings', icon: 'settings' },
|
|
23
24
|
];
|
|
24
25
|
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
export function renderTopNav() {
|
|
2
|
-
|
|
2
|
+
return `
|
|
3
3
|
<div class="top-nav-shell">
|
|
4
4
|
<a class="top-nav-brand" href="#/">SQLite Hub</a>
|
|
5
5
|
<div class="top-nav-actions">
|
|
6
|
-
<button class="top-nav-icon" data-action="refresh-view" type="button" aria-label="Refresh">
|
|
7
|
-
<span class="material-symbols-outlined">refresh</span>
|
|
8
|
-
</button>
|
|
9
6
|
<button
|
|
10
7
|
class="top-nav-icon"
|
|
11
8
|
data-action="open-modal"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { renderDropdownButton } from "./dropdownButton.js";
|
|
2
|
+
|
|
3
|
+
function buildOpenItem({ disabled, icon, label, target, tableName }) {
|
|
4
|
+
return {
|
|
5
|
+
action: "navigate",
|
|
6
|
+
dataAttributes: {
|
|
7
|
+
to: target,
|
|
8
|
+
},
|
|
9
|
+
disabled,
|
|
10
|
+
icon,
|
|
11
|
+
label,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function renderWorkspaceOpenDropdown({
|
|
16
|
+
align = "right",
|
|
17
|
+
destinations = [],
|
|
18
|
+
disabled = false,
|
|
19
|
+
tableName = "",
|
|
20
|
+
} = {}) {
|
|
21
|
+
const safeTableName = String(tableName ?? "").trim();
|
|
22
|
+
const items = destinations.map((destination) => {
|
|
23
|
+
if (destination.key === "sql-editor") {
|
|
24
|
+
return {
|
|
25
|
+
action: "open-table-in-sql-editor",
|
|
26
|
+
dataAttributes: {
|
|
27
|
+
tableName: safeTableName,
|
|
28
|
+
},
|
|
29
|
+
disabled: disabled || !safeTableName,
|
|
30
|
+
icon: "terminal",
|
|
31
|
+
label: "SQL Editor",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return buildOpenItem({
|
|
36
|
+
disabled: disabled || !safeTableName,
|
|
37
|
+
icon: destination.icon,
|
|
38
|
+
label: destination.label,
|
|
39
|
+
target: destination.target(safeTableName),
|
|
40
|
+
tableName: safeTableName,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return renderDropdownButton({
|
|
45
|
+
align,
|
|
46
|
+
disabled: disabled || !safeTableName,
|
|
47
|
+
icon: "open_in_new",
|
|
48
|
+
items,
|
|
49
|
+
label: "Open",
|
|
50
|
+
title: safeTableName ? `Open ${safeTableName}` : "Open table",
|
|
51
|
+
});
|
|
52
|
+
}
|
package/frontend/js/router.js
CHANGED
|
@@ -11,6 +11,8 @@ export function parseHash(hash = window.location.hash) {
|
|
|
11
11
|
switch (segments[0]) {
|
|
12
12
|
case 'connections':
|
|
13
13
|
return { name: 'connections', path: '/connections', params: {} };
|
|
14
|
+
case 'backups':
|
|
15
|
+
return { name: 'backups', path: '/backups', params: {} };
|
|
14
16
|
case 'overview':
|
|
15
17
|
return { name: 'overview', path: '/overview', params: {} };
|
|
16
18
|
case 'charts':
|