sqlite-hub 0.17.2 → 1.1.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 +143 -47
- package/bin/sqlite-hub.js +206 -444
- package/examples/api/queries.js +31 -0
- package/examples/api/rows.js +27 -0
- package/frontend/assets/mockups/charts_1_bars_1200.webp +0 -0
- package/frontend/assets/mockups/charts_2_pie_1200.webp +0 -0
- package/frontend/assets/mockups/charts_3_scatter_plot_1200.webp +0 -0
- package/frontend/assets/mockups/connections_1200.webp +0 -0
- package/frontend/assets/mockups/data_1_1200.webp +0 -0
- package/frontend/assets/mockups/data_2_row_editor_1200.webp +0 -0
- package/frontend/assets/mockups/documents_1200.webp +0 -0
- package/frontend/assets/mockups/media_tagging_1_setup_1200.webp +0 -0
- package/frontend/assets/mockups/media_tagging_2_tagging_queue_1200.webp +0 -0
- package/frontend/assets/mockups/media_tagging_3_media_viewer_1200.webp +0 -0
- package/frontend/assets/mockups/overview_1200.webp +0 -0
- package/frontend/assets/mockups/settings_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_1_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_2_query_details_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_3_export_column_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_4_export_column_as_markdown_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_5_export_query_result_1200.webp +0 -0
- package/frontend/assets/mockups/structure_1_1200.webp +0 -0
- package/frontend/assets/mockups/structure_2_inspector_1200.webp +0 -0
- package/frontend/assets/mockups/table_designer_1_1200.webp +0 -0
- package/frontend/assets/mockups/table_designer_2_checks_1200.webp +0 -0
- package/frontend/js/api.js +25 -0
- package/frontend/js/app.js +263 -34
- package/frontend/js/components/formControls.js +38 -0
- package/frontend/js/components/modal.js +77 -21
- package/frontend/js/components/queryChartRenderer.js +28 -3
- package/frontend/js/components/queryEditor.js +20 -24
- package/frontend/js/components/queryHistoryDetail.js +1 -1
- package/frontend/js/components/queryHistoryHeader.js +48 -0
- package/frontend/js/components/queryHistoryList.js +132 -0
- package/frontend/js/components/queryHistoryPanel.js +72 -136
- package/frontend/js/components/structureGraph.js +699 -89
- package/frontend/js/components/tableDesignerEditor.js +3 -5
- package/frontend/js/store.js +188 -7
- package/frontend/js/utils/exportFilenames.js +3 -1
- package/frontend/js/views/charts.js +320 -169
- package/frontend/js/views/connections.js +59 -64
- package/frontend/js/views/data.js +12 -20
- package/frontend/js/views/editor.js +0 -2
- package/frontend/js/views/settings.js +219 -5
- package/frontend/js/views/structure.js +27 -13
- package/frontend/styles/components.css +155 -0
- package/frontend/styles/structure-graph.css +140 -35
- package/frontend/styles/tailwind.generated.css +2 -2
- package/frontend/styles/views.css +12 -6
- package/package.json +7 -6
- package/server/middleware/apiTokenAuth.js +30 -0
- package/server/routes/connections.js +17 -0
- package/server/routes/export.js +89 -22
- package/server/routes/externalApi.js +304 -0
- package/server/routes/settings.js +90 -21
- package/server/server.js +22 -1
- package/server/services/apiTokenService.js +101 -0
- package/server/services/appInfoService.js +215 -0
- package/server/services/databaseCommandService.js +443 -0
- package/server/services/nativeFileDialogService.js +93 -1
- package/server/services/sqlite/exportService.js +307 -22
- package/server/services/storage/appStateStore.js +113 -0
- package/server/utils/errors.js +7 -0
- package/tests/api-token-auth.test.js +236 -0
- package/tests/cli-args.test.js +16 -3
- package/tests/cli-service-delegation.test.js +43 -0
- package/tests/connections-file-dialog-route.test.js +43 -0
- package/tests/copy-column-modal.test.js +34 -0
- package/tests/database-command-service.test.js +139 -0
- package/tests/export-blob.test.js +54 -1
- package/tests/export-filenames.test.js +4 -0
- package/tests/form-controls.test.js +34 -0
- package/tests/native-file-dialog.test.js +27 -0
- package/tests/settings-api-tokens-route.test.js +97 -0
- package/tests/settings-metadata.test.js +99 -1
- package/tests/settings-view.test.js +75 -0
- package/frontend/assets/mockups/connections.png +0 -0
- package/frontend/assets/mockups/data.png +0 -0
- package/frontend/assets/mockups/data_row_editor.png +0 -0
- package/frontend/assets/mockups/home.png +0 -0
- package/frontend/assets/mockups/sql_editor.png +0 -0
- package/frontend/assets/mockups/sql_editor_croped.png +0 -0
- package/frontend/assets/mockups/sql_editor_querydetail.png +0 -0
- package/frontend/assets/mockups/structure.png +0 -0
- package/frontend/assets/mockups/structure_inspector.png +0 -0
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
isMarkdownTodoCopyColumnMode,
|
|
8
8
|
} from "../utils/copyColumnExport.js";
|
|
9
9
|
import { renderConnectionLogo } from "./connectionLogo.js";
|
|
10
|
+
import { renderTextInput } from "./formControls.js";
|
|
10
11
|
import {
|
|
11
12
|
analyzeQueryChartResult,
|
|
12
13
|
getQueryChartTypeLabel,
|
|
@@ -27,13 +28,7 @@ function renderField({ label, name, type = "text", placeholder = "", value = ""
|
|
|
27
28
|
<span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
|
|
28
29
|
${escapeHtml(label)}
|
|
29
30
|
</span>
|
|
30
|
-
|
|
31
|
-
class="control-input w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
|
|
32
|
-
name="${escapeHtml(name)}"
|
|
33
|
-
placeholder="${escapeHtml(placeholder)}"
|
|
34
|
-
type="${escapeHtml(type)}"
|
|
35
|
-
value="${escapeHtml(value)}"
|
|
36
|
-
/>
|
|
31
|
+
${renderTextInput({ name, placeholder, type, value })}
|
|
37
32
|
</label>
|
|
38
33
|
`;
|
|
39
34
|
}
|
|
@@ -152,14 +147,33 @@ function renderError(error) {
|
|
|
152
147
|
`;
|
|
153
148
|
}
|
|
154
149
|
|
|
155
|
-
function renderOpenConnectionForm(modal) {
|
|
150
|
+
export function renderOpenConnectionForm(modal) {
|
|
156
151
|
return `
|
|
157
152
|
<form class="space-y-5" data-form="open-connection">
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
153
|
+
<label class="block space-y-2">
|
|
154
|
+
<span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
|
|
155
|
+
SQLite File Path
|
|
156
|
+
</span>
|
|
157
|
+
<span class="flex items-stretch gap-2">
|
|
158
|
+
${renderTextInput({
|
|
159
|
+
className: "min-w-0 flex-1",
|
|
160
|
+
dataAttributes: { openDatabasePath: true },
|
|
161
|
+
name: "path",
|
|
162
|
+
placeholder: "/absolute/path/to/database.sqlite",
|
|
163
|
+
})}
|
|
164
|
+
<button
|
|
165
|
+
class="standard-button flex-none"
|
|
166
|
+
data-action="choose-open-database-path"
|
|
167
|
+
type="button"
|
|
168
|
+
>
|
|
169
|
+
<span class="material-symbols-outlined text-sm">folder_open</span>
|
|
170
|
+
<span data-open-database-path-button-label>Browse...</span>
|
|
171
|
+
</button>
|
|
172
|
+
</span>
|
|
173
|
+
<span class="block text-[11px] leading-5 text-on-surface-variant/60">
|
|
174
|
+
Choose an existing SQLite file, or enter an absolute path manually.
|
|
175
|
+
</span>
|
|
176
|
+
</label>
|
|
163
177
|
${renderField({
|
|
164
178
|
label: "Label",
|
|
165
179
|
name: "label",
|
|
@@ -272,13 +286,12 @@ export function renderCreateDatabaseForm(modal) {
|
|
|
272
286
|
New SQLite File Path
|
|
273
287
|
</span>
|
|
274
288
|
<span class="flex items-stretch gap-2">
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
name
|
|
279
|
-
placeholder
|
|
280
|
-
|
|
281
|
-
/>
|
|
289
|
+
${renderTextInput({
|
|
290
|
+
className: "min-w-0 flex-1",
|
|
291
|
+
dataAttributes: { createDatabasePath: true },
|
|
292
|
+
name: "path",
|
|
293
|
+
placeholder: "/absolute/path/to/new-database.sqlite",
|
|
294
|
+
})}
|
|
282
295
|
<button
|
|
283
296
|
class="standard-button flex-none"
|
|
284
297
|
data-action="choose-create-database-path"
|
|
@@ -847,6 +860,28 @@ function renderDeleteDocumentForm(modal) {
|
|
|
847
860
|
].join("");
|
|
848
861
|
}
|
|
849
862
|
|
|
863
|
+
export function renderDeleteApiTokenForm(modal) {
|
|
864
|
+
return [
|
|
865
|
+
'<form class="space-y-5" data-form="delete-api-token-confirm"><div class="space-y-3">',
|
|
866
|
+
'<p class="text-sm leading-7 text-on-surface">Delete API token <span class="font-bold text-primary-container">',
|
|
867
|
+
escapeHtml(modal.tokenName ?? "API token"),
|
|
868
|
+
"</span>?</p>",
|
|
869
|
+
'<p class="text-sm leading-7 text-on-surface-variant/65">Requests using this token will immediately lose access to <span class="font-semibold text-on-surface">',
|
|
870
|
+
escapeHtml(modal.databaseLabel ?? "the active database"),
|
|
871
|
+
".</span></p>",
|
|
872
|
+
'<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-3 font-mono text-[10px] text-on-surface-variant/55">',
|
|
873
|
+
escapeHtml(modal.tokenPrefix ?? ""),
|
|
874
|
+
"...</div>",
|
|
875
|
+
"</div>",
|
|
876
|
+
renderError(modal.error),
|
|
877
|
+
'<div class="flex items-center justify-between gap-3 pt-2">',
|
|
878
|
+
'<button class="standard-button" data-action="close-modal" type="button">Cancel</button>',
|
|
879
|
+
'<button class="delete-button" type="submit">',
|
|
880
|
+
modal.submitting ? "Deleting..." : "Delete Token",
|
|
881
|
+
"</button></div></form>",
|
|
882
|
+
].join("");
|
|
883
|
+
}
|
|
884
|
+
|
|
850
885
|
function getDocumentInsertQueryTitle(query) {
|
|
851
886
|
return query?.displayTitle || query?.title || query?.previewSql || query?.rawSql || "Saved query";
|
|
852
887
|
}
|
|
@@ -983,6 +1018,20 @@ function getExportOptions() {
|
|
|
983
1018
|
meta: ".md",
|
|
984
1019
|
preview: ["| id | name |", "| --- | --- |"],
|
|
985
1020
|
},
|
|
1021
|
+
{
|
|
1022
|
+
label: "JSON",
|
|
1023
|
+
format: "json",
|
|
1024
|
+
icon: "data_object",
|
|
1025
|
+
meta: ".json",
|
|
1026
|
+
preview: ['[', ' {"id":1,"name":"Acme"}', ']'],
|
|
1027
|
+
},
|
|
1028
|
+
{
|
|
1029
|
+
label: "Parquet",
|
|
1030
|
+
format: "parquet",
|
|
1031
|
+
icon: "dataset",
|
|
1032
|
+
meta: ".parquet",
|
|
1033
|
+
preview: ["columnar binary", "typed row groups"],
|
|
1034
|
+
},
|
|
986
1035
|
{
|
|
987
1036
|
label: "Duplicate",
|
|
988
1037
|
format: "table",
|
|
@@ -1012,7 +1061,7 @@ function renderTextExportModal(modal, action) {
|
|
|
1012
1061
|
${disabledAttribute}
|
|
1013
1062
|
/>
|
|
1014
1063
|
</label>
|
|
1015
|
-
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
1064
|
+
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
|
|
1016
1065
|
${getExportOptions()
|
|
1017
1066
|
.map(
|
|
1018
1067
|
(option) => `
|
|
@@ -1687,6 +1736,11 @@ export function renderModal(state) {
|
|
|
1687
1736
|
title: "Delete Document",
|
|
1688
1737
|
body: renderDeleteDocumentForm(modal),
|
|
1689
1738
|
},
|
|
1739
|
+
"delete-api-token": {
|
|
1740
|
+
eyebrow: "Settings // Confirm token deletion",
|
|
1741
|
+
title: "Delete API Token",
|
|
1742
|
+
body: renderDeleteApiTokenForm(modal),
|
|
1743
|
+
},
|
|
1690
1744
|
"document-insert-table": {
|
|
1691
1745
|
eyebrow: "Documents // Saved query output",
|
|
1692
1746
|
title: "Insert Table",
|
|
@@ -1744,6 +1798,8 @@ export function renderModal(state) {
|
|
|
1744
1798
|
modal.kind === "row-update-preview" ||
|
|
1745
1799
|
modal.kind === "table-designer-constraints"
|
|
1746
1800
|
? "max-w-3xl"
|
|
1801
|
+
: modal.kind === "query-export" || modal.kind === "data-export"
|
|
1802
|
+
? "max-w-4xl"
|
|
1747
1803
|
: "max-w-xl"
|
|
1748
1804
|
} border border-outline-variant/20 bg-surface-container shadow-[0_24px_80px_rgba(0,0,0,0.45)]">
|
|
1749
1805
|
<div class="flex items-start justify-between gap-4 border-b border-outline-variant/10 bg-surface-container-low px-6 py-5">
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import { analyzeQueryChartResult, validateQueryChartConfig } from "../lib/queryCharts.js";
|
|
8
8
|
|
|
9
9
|
const chartInstances = new Map();
|
|
10
|
+
const chartHosts = new Map();
|
|
10
11
|
const resizeObservers = new Map();
|
|
11
12
|
|
|
12
13
|
function getEchartsRuntime() {
|
|
@@ -41,6 +42,8 @@ function disposeChart(chartId) {
|
|
|
41
42
|
chartInstances.delete(chartId);
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
chartHosts.delete(chartId);
|
|
46
|
+
|
|
44
47
|
const observer = resizeObservers.get(chartId);
|
|
45
48
|
|
|
46
49
|
if (observer) {
|
|
@@ -58,7 +61,15 @@ export function mountQueryChartRenderer(state) {
|
|
|
58
61
|
const charts = state.charts.detail?.charts ?? [];
|
|
59
62
|
const result = state.charts.result;
|
|
60
63
|
|
|
61
|
-
|
|
64
|
+
const activeChartIds = new Set(charts.map((chart) => String(chart.id)));
|
|
65
|
+
|
|
66
|
+
Array.from(chartInstances.keys()).forEach((chartId) => {
|
|
67
|
+
const host = chartHosts.get(chartId);
|
|
68
|
+
|
|
69
|
+
if (!activeChartIds.has(chartId) || !(host instanceof HTMLElement) || !host.isConnected) {
|
|
70
|
+
disposeChart(chartId);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
62
73
|
|
|
63
74
|
if (!echartsRuntime || !result || !charts.length) {
|
|
64
75
|
return;
|
|
@@ -85,17 +96,31 @@ export function mountQueryChartRenderer(state) {
|
|
|
85
96
|
return;
|
|
86
97
|
}
|
|
87
98
|
|
|
99
|
+
const chartId = String(chart.id);
|
|
100
|
+
const existingInstance = chartInstances.get(chartId);
|
|
101
|
+
const existingHost = chartHosts.get(chartId);
|
|
102
|
+
|
|
103
|
+
if (existingInstance && existingHost === host) {
|
|
104
|
+
existingInstance.resize();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (existingInstance) {
|
|
109
|
+
disposeChart(chartId);
|
|
110
|
+
}
|
|
111
|
+
|
|
88
112
|
const instance = echartsRuntime.init(host);
|
|
89
113
|
|
|
90
114
|
instance.setOption(buildOption(chart, result.rows ?? [], analysis), true);
|
|
91
|
-
chartInstances.set(
|
|
115
|
+
chartInstances.set(chartId, instance);
|
|
116
|
+
chartHosts.set(chartId, host);
|
|
92
117
|
|
|
93
118
|
const resizeObserver = new ResizeObserver(() => {
|
|
94
119
|
instance.resize();
|
|
95
120
|
});
|
|
96
121
|
|
|
97
122
|
resizeObserver.observe(host);
|
|
98
|
-
resizeObservers.set(
|
|
123
|
+
resizeObservers.set(chartId, resizeObserver);
|
|
99
124
|
});
|
|
100
125
|
}
|
|
101
126
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { escapeHtml, highlightSql } from "../utils/format.js";
|
|
2
|
-
import { renderActionBar } from "./actionBar.js";
|
|
3
2
|
|
|
4
3
|
function renderLineNumbers(query) {
|
|
5
4
|
const lineCount = Math.max(1, String(query || "").split("\n").length);
|
|
@@ -40,20 +39,11 @@ export function renderQueryEditor({
|
|
|
40
39
|
query,
|
|
41
40
|
executing = false,
|
|
42
41
|
exporting = false,
|
|
43
|
-
historyLoading = false,
|
|
44
|
-
historyTotal = 0,
|
|
45
42
|
editorVisible = true,
|
|
46
43
|
historyVisible = true,
|
|
47
44
|
}) {
|
|
48
45
|
const secondaryButtonClass = "standard-button";
|
|
49
46
|
const left = `
|
|
50
|
-
<div class="flex items-center gap-2 text-[10px] font-mono uppercase tracking-widest text-on-surface-variant/40">
|
|
51
|
-
<span class="material-symbols-outlined text-xs">history</span>
|
|
52
|
-
${historyLoading ? "Loading history..." : `${historyTotal} queries tracked`}
|
|
53
|
-
</div>
|
|
54
|
-
`;
|
|
55
|
-
|
|
56
|
-
const right = `
|
|
57
47
|
<button
|
|
58
48
|
class="${secondaryButtonClass}"
|
|
59
49
|
data-action="toggle-editor-panel"
|
|
@@ -63,15 +53,9 @@ export function renderQueryEditor({
|
|
|
63
53
|
<span class="material-symbols-outlined text-sm">${editorVisible ? "keyboard_arrow_down" : "terminal"}</span>
|
|
64
54
|
${editorVisible ? "Hide Editor" : "Show Editor"}
|
|
65
55
|
</button>
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
data-next-value="${historyVisible ? "false" : "true"}"
|
|
70
|
-
type="button"
|
|
71
|
-
>
|
|
72
|
-
<span class="material-symbols-outlined text-sm">${historyVisible ? "visibility_off" : "visibility"}</span>
|
|
73
|
-
${historyVisible ? "Hide History" : "Show History"}
|
|
74
|
-
</button>
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
const center = `
|
|
75
59
|
<button
|
|
76
60
|
class="${secondaryButtonClass}"
|
|
77
61
|
data-action="format-current-query"
|
|
@@ -104,14 +88,26 @@ export function renderQueryEditor({
|
|
|
104
88
|
</button>
|
|
105
89
|
`;
|
|
106
90
|
|
|
91
|
+
const right = `
|
|
92
|
+
<button
|
|
93
|
+
class="${secondaryButtonClass}"
|
|
94
|
+
data-action="toggle-query-history-panel"
|
|
95
|
+
data-next-value="${historyVisible ? "false" : "true"}"
|
|
96
|
+
type="button"
|
|
97
|
+
>
|
|
98
|
+
<span class="material-symbols-outlined text-sm">${historyVisible ? "visibility_off" : "visibility"}</span>
|
|
99
|
+
${historyVisible ? "Hide History" : "Show History"}
|
|
100
|
+
</button>
|
|
101
|
+
`;
|
|
102
|
+
|
|
107
103
|
return `
|
|
108
104
|
<div class="flex h-full min-h-0 flex-col">
|
|
109
105
|
<div class="bg-surface-container-low px-6 py-3">
|
|
110
|
-
|
|
111
|
-
left
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
106
|
+
<div class="query-editor-toolbar">
|
|
107
|
+
<div class="query-editor-toolbar__left">${left}</div>
|
|
108
|
+
<div class="query-editor-toolbar__center">${center}</div>
|
|
109
|
+
<div class="query-editor-toolbar__right">${right}</div>
|
|
110
|
+
</div>
|
|
115
111
|
</div>
|
|
116
112
|
${
|
|
117
113
|
editorVisible
|
|
@@ -163,7 +163,7 @@ export function renderQueryHistoryDetail({
|
|
|
163
163
|
escapeHtml(item.displayTitle),
|
|
164
164
|
"</h2></div>",
|
|
165
165
|
'<button class="query-history-icon-button" data-action="clear-query-history-selection" type="button"><span class="material-symbols-outlined text-[18px]">close</span></button>',
|
|
166
|
-
'</div><div class="mt-4 flex flex-wrap gap-2">',
|
|
166
|
+
'</div><div class="query-history-badge-row mt-4 flex flex-wrap gap-2">',
|
|
167
167
|
statusMarkup,
|
|
168
168
|
"</div></div>",
|
|
169
169
|
'<div class="custom-scrollbar min-h-0 flex-1 overflow-auto px-5 py-5">',
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { escapeHtml } from '../utils/format.js';
|
|
2
|
+
|
|
3
|
+
export function renderQueryHistoryHeader({
|
|
4
|
+
title = 'Query History',
|
|
5
|
+
icon = 'history',
|
|
6
|
+
closeAction = 'toggle-query-history-panel',
|
|
7
|
+
closeTitle = 'Hide query history',
|
|
8
|
+
nextValue = 'false',
|
|
9
|
+
} = {}) {
|
|
10
|
+
return `
|
|
11
|
+
<div class="flex items-center justify-between gap-3">
|
|
12
|
+
<div class="flex items-center gap-2">
|
|
13
|
+
<span class="material-symbols-outlined text-[18px] text-primary-container">${escapeHtml(icon)}</span>
|
|
14
|
+
<span class="font-headline text-xs font-black uppercase tracking-[0.18em] text-primary-container">
|
|
15
|
+
${escapeHtml(title)}
|
|
16
|
+
</span>
|
|
17
|
+
</div>
|
|
18
|
+
<button
|
|
19
|
+
class="query-history-icon-button"
|
|
20
|
+
data-action="${escapeHtml(closeAction)}"
|
|
21
|
+
data-next-value="${escapeHtml(nextValue)}"
|
|
22
|
+
title="${escapeHtml(closeTitle)}"
|
|
23
|
+
type="button"
|
|
24
|
+
>
|
|
25
|
+
<span class="material-symbols-outlined text-[18px]">close</span>
|
|
26
|
+
</button>
|
|
27
|
+
</div>
|
|
28
|
+
`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function renderQueryHistorySearch({
|
|
32
|
+
bind = 'query-history-search',
|
|
33
|
+
value = '',
|
|
34
|
+
placeholder = 'Search SQL, titles, notes...',
|
|
35
|
+
} = {}) {
|
|
36
|
+
return `
|
|
37
|
+
<label class="mt-4 block">
|
|
38
|
+
<span class="sr-only">Search query history</span>
|
|
39
|
+
<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
|
+
data-bind="${escapeHtml(bind)}"
|
|
42
|
+
placeholder="${escapeHtml(placeholder)}"
|
|
43
|
+
type="search"
|
|
44
|
+
value="${escapeHtml(value)}"
|
|
45
|
+
/>
|
|
46
|
+
</label>
|
|
47
|
+
`;
|
|
48
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { escapeHtml, formatNumber } from '../utils/format.js';
|
|
2
|
+
|
|
3
|
+
export function getQueryTypeTone(queryType) {
|
|
4
|
+
if (queryType === 'select' || queryType === 'update') {
|
|
5
|
+
return 'success';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (queryType === 'pragma') {
|
|
9
|
+
return 'primary';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return 'muted';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function renderQueryHistoryBadgeRow(markup, { compact = true, className = '' } = {}) {
|
|
16
|
+
const rowClasses = ['query-history-badge-row', compact ? 'query-history-badge-row--compact' : '', className]
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
.join(' ');
|
|
19
|
+
|
|
20
|
+
return `<div class="${rowClasses}">${markup}</div>`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function renderQueryHistoryTabs({ tabs = [], activeTab = 'recent', action, count = null, countAttr = '' }) {
|
|
24
|
+
const activeCount = count ?? tabs.find(tab => tab.id === activeTab)?.count ?? 0;
|
|
25
|
+
|
|
26
|
+
return [
|
|
27
|
+
'<div class="query-history-tabs">',
|
|
28
|
+
tabs
|
|
29
|
+
.map(tab =>
|
|
30
|
+
[
|
|
31
|
+
'<button class="query-history-tab ',
|
|
32
|
+
activeTab === tab.id ? 'is-active' : '',
|
|
33
|
+
'" data-action="',
|
|
34
|
+
escapeHtml(action),
|
|
35
|
+
'" data-tab="',
|
|
36
|
+
escapeHtml(tab.id),
|
|
37
|
+
'" type="button">',
|
|
38
|
+
escapeHtml(tab.label),
|
|
39
|
+
'</button>',
|
|
40
|
+
].join(''),
|
|
41
|
+
)
|
|
42
|
+
.join(''),
|
|
43
|
+
'<span class="query-history-tabs__count" ',
|
|
44
|
+
countAttr,
|
|
45
|
+
'>',
|
|
46
|
+
escapeHtml(formatNumber(activeCount)),
|
|
47
|
+
'</span></div>',
|
|
48
|
+
].join('');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function renderQueryHistoryIconButton({
|
|
52
|
+
action,
|
|
53
|
+
historyId = null,
|
|
54
|
+
icon,
|
|
55
|
+
title,
|
|
56
|
+
active = false,
|
|
57
|
+
nextValue = null,
|
|
58
|
+
attrs = '',
|
|
59
|
+
}) {
|
|
60
|
+
return [
|
|
61
|
+
'<button class="query-history-icon-button ',
|
|
62
|
+
active ? 'is-active' : '',
|
|
63
|
+
'" data-action="',
|
|
64
|
+
escapeHtml(action),
|
|
65
|
+
'"',
|
|
66
|
+
historyId === null || historyId === undefined ? '' : ` data-history-id="${escapeHtml(historyId)}"`,
|
|
67
|
+
nextValue === null || nextValue === undefined ? '' : ` data-next-value="${escapeHtml(nextValue)}"`,
|
|
68
|
+
attrs ? ` ${attrs}` : '',
|
|
69
|
+
' title="',
|
|
70
|
+
escapeHtml(title),
|
|
71
|
+
'" type="button"><span class="material-symbols-outlined text-[18px]">',
|
|
72
|
+
escapeHtml(icon),
|
|
73
|
+
'</span></button>',
|
|
74
|
+
].join('');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function renderQueryHistoryActionGroup(actions = []) {
|
|
78
|
+
return `<div class="query-history-item-actions">${actions.filter(Boolean).join('')}</div>`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function renderQueryHistoryListItem({
|
|
82
|
+
title,
|
|
83
|
+
preview,
|
|
84
|
+
historyId,
|
|
85
|
+
active = false,
|
|
86
|
+
error = false,
|
|
87
|
+
hitAction,
|
|
88
|
+
hitAttrs = '',
|
|
89
|
+
itemAttrs = '',
|
|
90
|
+
titleAttrs = '',
|
|
91
|
+
badgesMarkup = '',
|
|
92
|
+
footerMetaMarkup = '',
|
|
93
|
+
actionsMarkup = '',
|
|
94
|
+
}) {
|
|
95
|
+
const itemClasses = ['query-history-item', active ? 'is-active' : '', error ? 'is-error' : '']
|
|
96
|
+
.filter(Boolean)
|
|
97
|
+
.join(' ');
|
|
98
|
+
const footerMeta = String(footerMetaMarkup ?? '').trim()
|
|
99
|
+
? ['<div class="query-history-item-meta">', footerMetaMarkup, '</div>'].join('')
|
|
100
|
+
: '';
|
|
101
|
+
|
|
102
|
+
return [
|
|
103
|
+
'<article class="',
|
|
104
|
+
itemClasses,
|
|
105
|
+
'"',
|
|
106
|
+
itemAttrs ? ` ${itemAttrs}` : '',
|
|
107
|
+
'><button class="query-history-item-hit ',
|
|
108
|
+
active ? 'is-active' : '',
|
|
109
|
+
'" data-action="',
|
|
110
|
+
escapeHtml(hitAction),
|
|
111
|
+
'" data-history-id="',
|
|
112
|
+
escapeHtml(historyId),
|
|
113
|
+
'"',
|
|
114
|
+
hitAttrs ? ` ${hitAttrs}` : '',
|
|
115
|
+
' type="button">',
|
|
116
|
+
'<div class="query-history-item-main">',
|
|
117
|
+
'<span class="query-history-item-title"',
|
|
118
|
+
titleAttrs ? ` ${titleAttrs}` : '',
|
|
119
|
+
'>',
|
|
120
|
+
escapeHtml(title),
|
|
121
|
+
'</span>',
|
|
122
|
+
badgesMarkup,
|
|
123
|
+
'</div>',
|
|
124
|
+
'<p class="query-history-sql-preview">',
|
|
125
|
+
escapeHtml(preview),
|
|
126
|
+
'</p></button>',
|
|
127
|
+
'<div class="query-history-item-footer">',
|
|
128
|
+
footerMeta,
|
|
129
|
+
actionsMarkup,
|
|
130
|
+
'</div></article>',
|
|
131
|
+
].join('');
|
|
132
|
+
}
|