sqlite-hub 1.0.0 → 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 +47 -33
- package/bin/sqlite-hub.js +127 -47
- package/frontend/js/api.js +6 -0
- package/frontend/js/app.js +199 -34
- package/frontend/js/components/modal.js +17 -1
- 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 +73 -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 +78 -0
- 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 +1 -1
- package/frontend/styles/views.css +12 -6
- package/package.json +3 -2
- package/server/routes/export.js +89 -22
- package/server/routes/externalApi.js +84 -2
- package/server/routes/settings.js +37 -28
- package/server/services/appInfoService.js +215 -0
- package/server/services/databaseCommandService.js +50 -6
- package/server/services/sqlite/exportService.js +307 -22
- package/tests/api-token-auth.test.js +110 -1
- package/tests/cli-args.test.js +16 -3
- package/tests/database-command-service.test.js +39 -2
- package/tests/export-blob.test.js +54 -1
- package/tests/export-filenames.test.js +4 -0
- package/tests/settings-api-tokens-route.test.js +22 -1
- package/tests/settings-metadata.test.js +99 -1
- package/tests/settings-view.test.js +28 -0
package/frontend/js/app.js
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
clearEditorResults,
|
|
36
36
|
clearQueryHistorySelection,
|
|
37
37
|
closeModal,
|
|
38
|
+
checkSettingsAppVersion,
|
|
38
39
|
dismissMediaTaggingIssue,
|
|
39
40
|
dismissToast,
|
|
40
41
|
executeCurrentQuery,
|
|
@@ -95,7 +96,9 @@ import {
|
|
|
95
96
|
toggleDataTablesPanel,
|
|
96
97
|
setCurrentQuery,
|
|
97
98
|
setChartsHeightPreset,
|
|
99
|
+
setChartsDetailPanelVisibility,
|
|
98
100
|
setChartsHistoryTab,
|
|
101
|
+
setChartsHistorySearchInput,
|
|
99
102
|
setChartsHistoryPanelVisibility,
|
|
100
103
|
setEditorPanelVisibility,
|
|
101
104
|
setEditorTab,
|
|
@@ -222,6 +225,7 @@ let lastRenderedPanelMarkup = '';
|
|
|
222
225
|
let lastRenderedModalMarkup = '';
|
|
223
226
|
let lastRenderedToastMarkup = '';
|
|
224
227
|
let lastRenderedChartsHistorySignature = '';
|
|
228
|
+
let lastRenderedChartsCardSignature = '';
|
|
225
229
|
let lastRenderedPanelOpen = false;
|
|
226
230
|
let lastRenderedLockedRoute = false;
|
|
227
231
|
let pendingNewTableDesignerAutofocus = false;
|
|
@@ -714,7 +718,11 @@ function buildChartsHistorySignature(state) {
|
|
|
714
718
|
}
|
|
715
719
|
|
|
716
720
|
return JSON.stringify({
|
|
721
|
+
detailPanelVisible: Boolean(state.charts.detailPanelVisible),
|
|
722
|
+
detailPanelHistoryId: state.charts.detailPanelVisible ? state.charts.selectedHistoryId : null,
|
|
717
723
|
tab: state.charts.historyTab ?? 'recent',
|
|
724
|
+
searchInput: state.charts.historySearchInput ?? '',
|
|
725
|
+
search: state.charts.historySearch ?? '',
|
|
718
726
|
queries: queries.map(item => [
|
|
719
727
|
item.id,
|
|
720
728
|
item.displayTitle,
|
|
@@ -725,6 +733,46 @@ function buildChartsHistorySignature(state) {
|
|
|
725
733
|
});
|
|
726
734
|
}
|
|
727
735
|
|
|
736
|
+
const chartSignatureObjectIds = new WeakMap();
|
|
737
|
+
let nextChartSignatureObjectId = 1;
|
|
738
|
+
|
|
739
|
+
function getChartSignatureObjectId(value) {
|
|
740
|
+
if (!value || typeof value !== 'object') {
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if (!chartSignatureObjectIds.has(value)) {
|
|
745
|
+
chartSignatureObjectIds.set(value, nextChartSignatureObjectId);
|
|
746
|
+
nextChartSignatureObjectId += 1;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
return chartSignatureObjectIds.get(value);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function buildChartsCardSignature(state) {
|
|
753
|
+
if (state.route.name !== 'charts' || !state.charts.selectedHistoryId || !state.charts.detail?.item) {
|
|
754
|
+
return '';
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const charts = state.charts.detail?.charts ?? [];
|
|
758
|
+
|
|
759
|
+
return JSON.stringify({
|
|
760
|
+
selectedHistoryId: state.charts.selectedHistoryId,
|
|
761
|
+
chartHeightPreset: state.charts.chartHeightPreset ?? 'medium',
|
|
762
|
+
resultObjectId: getChartSignatureObjectId(state.charts.result),
|
|
763
|
+
resultLoading: Boolean(state.charts.resultLoading),
|
|
764
|
+
resultError: state.charts.resultError
|
|
765
|
+
? [state.charts.resultError.code ?? '', state.charts.resultError.message ?? '']
|
|
766
|
+
: null,
|
|
767
|
+
charts: charts.map(chart => [
|
|
768
|
+
chart.id,
|
|
769
|
+
chart.name,
|
|
770
|
+
chart.chartType,
|
|
771
|
+
JSON.stringify(chart.config ?? {}),
|
|
772
|
+
]),
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
|
|
728
776
|
function syncChartsHistorySelectionUi(state) {
|
|
729
777
|
const selectedHistoryId = String(state.charts.selectedHistoryId ?? '');
|
|
730
778
|
const historyButtons = shellRefs.view.querySelectorAll(
|
|
@@ -738,56 +786,123 @@ function syncChartsHistorySelectionUi(state) {
|
|
|
738
786
|
|
|
739
787
|
const itemNode = button.closest('[data-charts-history-item]');
|
|
740
788
|
const isSelected = button.dataset.historyId === selectedHistoryId;
|
|
741
|
-
const titleNode = button.querySelector('[data-charts-history-title]');
|
|
742
789
|
const selectionNode = itemNode instanceof HTMLElement ? itemNode : button;
|
|
743
790
|
|
|
744
|
-
selectionNode.classList.toggle('
|
|
745
|
-
|
|
746
|
-
selectionNode.classList.toggle('border-outline-variant/10', !isSelected);
|
|
747
|
-
selectionNode.classList.toggle('bg-surface-container-lowest', !isSelected);
|
|
748
|
-
selectionNode.classList.toggle('hover:bg-surface-container-high', !isSelected);
|
|
749
|
-
|
|
750
|
-
if (titleNode instanceof HTMLElement) {
|
|
751
|
-
titleNode.classList.toggle('text-primary-container', isSelected);
|
|
752
|
-
titleNode.classList.toggle('text-on-surface', !isSelected);
|
|
753
|
-
}
|
|
791
|
+
selectionNode.classList.toggle('is-active', isSelected);
|
|
792
|
+
button.classList.toggle('is-active', isSelected);
|
|
754
793
|
}
|
|
755
794
|
|
|
756
795
|
return true;
|
|
757
796
|
}
|
|
758
797
|
|
|
759
|
-
function
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
actionNode.dataset.nextValue = nextValue ? 'false' : 'true';
|
|
764
|
-
actionNode.title = nextValue ? 'Remove from saved' : 'Save query';
|
|
798
|
+
function syncChartsSavedToggleButtonUi(button, nextValue) {
|
|
799
|
+
button.classList.toggle('is-active', nextValue);
|
|
800
|
+
button.dataset.nextValue = nextValue ? 'false' : 'true';
|
|
801
|
+
button.title = nextValue ? 'Remove from saved' : 'Save query';
|
|
765
802
|
|
|
766
|
-
const iconNode =
|
|
803
|
+
const iconNode = button.querySelector('.material-symbols-outlined');
|
|
767
804
|
if (iconNode instanceof HTMLElement) {
|
|
768
805
|
iconNode.textContent = nextValue ? 'bookmark' : 'bookmark_add';
|
|
769
806
|
}
|
|
770
807
|
|
|
771
|
-
|
|
772
|
-
|
|
808
|
+
const labelNode = button.querySelector('[data-charts-saved-label]');
|
|
809
|
+
if (labelNode instanceof HTMLElement) {
|
|
810
|
+
labelNode.textContent = nextValue ? 'Unsave' : 'Save';
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function syncChartsSavedToggleUi(actionNode, nextValue) {
|
|
815
|
+
const historyId = actionNode.dataset.historyId;
|
|
816
|
+
const relatedButtons = [
|
|
817
|
+
actionNode,
|
|
818
|
+
...shellRefs.view.querySelectorAll('[data-action="toggle-charts-query-history-saved"][data-history-id]'),
|
|
819
|
+
...shellRefs.panel.querySelectorAll('[data-action="toggle-charts-query-history-saved"][data-history-id]'),
|
|
820
|
+
].filter((button, index, buttons) => {
|
|
821
|
+
return (
|
|
822
|
+
button instanceof HTMLElement &&
|
|
823
|
+
button.dataset.historyId === historyId &&
|
|
824
|
+
buttons.indexOf(button) === index
|
|
825
|
+
);
|
|
826
|
+
});
|
|
773
827
|
|
|
774
|
-
|
|
828
|
+
for (const button of relatedButtons) {
|
|
829
|
+
syncChartsSavedToggleButtonUi(button, nextValue);
|
|
830
|
+
|
|
831
|
+
const itemNode = button.closest('[data-charts-history-item]');
|
|
832
|
+
const historyTab = getState().charts.historyTab;
|
|
833
|
+
if (
|
|
834
|
+
itemNode instanceof HTMLElement &&
|
|
835
|
+
((historyTab === 'saved' && !nextValue) || (historyTab === 'unsaved' && nextValue))
|
|
836
|
+
) {
|
|
775
837
|
itemNode.remove();
|
|
776
838
|
}
|
|
777
839
|
}
|
|
778
840
|
|
|
841
|
+
for (const badgeNode of [
|
|
842
|
+
...shellRefs.view.querySelectorAll('[data-charts-saved-badge][data-history-id]'),
|
|
843
|
+
...shellRefs.panel.querySelectorAll('[data-charts-saved-badge][data-history-id]'),
|
|
844
|
+
]) {
|
|
845
|
+
if (badgeNode instanceof HTMLElement && badgeNode.dataset.historyId === historyId) {
|
|
846
|
+
badgeNode.toggleAttribute('hidden', !nextValue);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
779
850
|
const countNode = shellRefs.view.querySelector('[data-charts-history-count]');
|
|
780
851
|
if (countNode instanceof HTMLElement) {
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
852
|
+
countNode.textContent = String(shellRefs.view.querySelectorAll('[data-charts-history-item]').length);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function renderChartsMainIntoScratch(state) {
|
|
857
|
+
const scratch = document.createElement('div');
|
|
858
|
+
|
|
859
|
+
replaceChildrenFromRenderedMarkup(scratch, renderChartsView(state).main);
|
|
860
|
+
return scratch;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function patchChartsHistoryUi(state) {
|
|
864
|
+
const chartsView = shellRefs.view.querySelector('.charts-view');
|
|
865
|
+
|
|
866
|
+
if (!(chartsView instanceof HTMLElement)) {
|
|
867
|
+
return false;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const currentSidebar = chartsView.querySelector('.charts-view__sidebar');
|
|
871
|
+
const scratch = renderChartsMainIntoScratch(state);
|
|
872
|
+
const nextSidebar = scratch.querySelector('.charts-view__sidebar');
|
|
873
|
+
|
|
874
|
+
if (!currentSidebar && !nextSidebar) {
|
|
875
|
+
return true;
|
|
787
876
|
}
|
|
877
|
+
|
|
878
|
+
if (!(currentSidebar instanceof HTMLElement) || !(nextSidebar instanceof HTMLElement)) {
|
|
879
|
+
return false;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
currentSidebar.replaceWith(nextSidebar);
|
|
883
|
+
return true;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function renderChartsDetailIntoScratch(state) {
|
|
887
|
+
const scratch = document.createElement('div');
|
|
888
|
+
|
|
889
|
+
replaceChildrenFromRenderedMarkup(scratch, renderChartsDetail(state));
|
|
890
|
+
return scratch;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function replaceChartsDetailSection(detailNode, scratchNode, selector) {
|
|
894
|
+
const currentNode = detailNode.querySelector(selector);
|
|
895
|
+
const nextNode = scratchNode.querySelector(selector);
|
|
896
|
+
|
|
897
|
+
if (!(currentNode instanceof HTMLElement) || !(nextNode instanceof HTMLElement)) {
|
|
898
|
+
return false;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
currentNode.replaceWith(nextNode);
|
|
902
|
+
return true;
|
|
788
903
|
}
|
|
789
904
|
|
|
790
|
-
function patchChartsDetailUi(state) {
|
|
905
|
+
function patchChartsDetailUi(state, { preserveCharts = false } = {}) {
|
|
791
906
|
const chartsView = shellRefs.view.querySelector('.charts-view');
|
|
792
907
|
const detailNode = chartsView?.querySelector('.charts-view__detail');
|
|
793
908
|
|
|
@@ -802,7 +917,17 @@ function patchChartsDetailUi(state) {
|
|
|
802
917
|
return false;
|
|
803
918
|
}
|
|
804
919
|
|
|
805
|
-
|
|
920
|
+
if (preserveCharts) {
|
|
921
|
+
const scratch = renderChartsDetailIntoScratch(state);
|
|
922
|
+
const patched = replaceChartsDetailSection(detailNode, scratch, '[data-charts-detail-header]');
|
|
923
|
+
|
|
924
|
+
if (!patched) {
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
927
|
+
} else {
|
|
928
|
+
replaceChildrenFromRenderedMarkup(detailNode, renderChartsDetail(state));
|
|
929
|
+
}
|
|
930
|
+
|
|
806
931
|
syncChartsHistorySelectionUi(state);
|
|
807
932
|
return true;
|
|
808
933
|
}
|
|
@@ -1228,6 +1353,7 @@ function renderApp(state) {
|
|
|
1228
1353
|
const modalMarkup = renderModal(state);
|
|
1229
1354
|
const toastMarkup = renderToasts(state.toasts);
|
|
1230
1355
|
const chartsHistorySignature = buildChartsHistorySignature(state);
|
|
1356
|
+
const chartsCardSignature = buildChartsCardSignature(state);
|
|
1231
1357
|
const isLockedRoute = [
|
|
1232
1358
|
'editor',
|
|
1233
1359
|
'editorResults',
|
|
@@ -1247,6 +1373,7 @@ function renderApp(state) {
|
|
|
1247
1373
|
const panelChanged = panel !== lastRenderedPanelMarkup;
|
|
1248
1374
|
const modalChanged = modalMarkup !== lastRenderedModalMarkup;
|
|
1249
1375
|
const chartsHistoryChanged = chartsHistorySignature !== lastRenderedChartsHistorySignature;
|
|
1376
|
+
const chartsCardsChanged = chartsCardSignature !== lastRenderedChartsCardSignature;
|
|
1250
1377
|
const panelOpenChanged = panelOpen !== lastRenderedPanelOpen;
|
|
1251
1378
|
const lockedRouteChanged = isLockedRoute !== lastRenderedLockedRoute;
|
|
1252
1379
|
const shellMarkupUnchanged =
|
|
@@ -1286,12 +1413,25 @@ function renderApp(state) {
|
|
|
1286
1413
|
}
|
|
1287
1414
|
|
|
1288
1415
|
const canPatchChartsMain =
|
|
1289
|
-
mainChanged && previousRouteName === 'charts' && state.route.name === 'charts'
|
|
1416
|
+
mainChanged && previousRouteName === 'charts' && state.route.name === 'charts';
|
|
1290
1417
|
let mainPatched = false;
|
|
1418
|
+
let preservedChartsDom = false;
|
|
1291
1419
|
|
|
1292
1420
|
if (canPatchChartsMain) {
|
|
1293
|
-
|
|
1294
|
-
|
|
1421
|
+
const historyPatched = !chartsHistoryChanged || patchChartsHistoryUi(state);
|
|
1422
|
+
|
|
1423
|
+
if (historyPatched) {
|
|
1424
|
+
if (chartsCardsChanged) {
|
|
1425
|
+
teardownQueryChartRenderer();
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
preservedChartsDom = !chartsCardsChanged;
|
|
1429
|
+
mainPatched = patchChartsDetailUi(state, { preserveCharts: preservedChartsDom });
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
if (!mainPatched) {
|
|
1433
|
+
preservedChartsDom = false;
|
|
1434
|
+
}
|
|
1295
1435
|
}
|
|
1296
1436
|
|
|
1297
1437
|
if (mainChanged) {
|
|
@@ -1387,6 +1527,7 @@ function renderApp(state) {
|
|
|
1387
1527
|
lastRenderedModalMarkup = modalMarkup;
|
|
1388
1528
|
lastRenderedToastMarkup = toastMarkup;
|
|
1389
1529
|
lastRenderedChartsHistorySignature = chartsHistorySignature;
|
|
1530
|
+
lastRenderedChartsCardSignature = chartsCardSignature;
|
|
1390
1531
|
lastRenderedPanelOpen = panelOpen;
|
|
1391
1532
|
lastRenderedLockedRoute = isLockedRoute;
|
|
1392
1533
|
|
|
@@ -1396,7 +1537,7 @@ function renderApp(state) {
|
|
|
1396
1537
|
});
|
|
1397
1538
|
}
|
|
1398
1539
|
|
|
1399
|
-
if (state.route.name === 'charts') {
|
|
1540
|
+
if (state.route.name === 'charts' && !preservedChartsDom) {
|
|
1400
1541
|
mountQueryChartRenderer(state);
|
|
1401
1542
|
}
|
|
1402
1543
|
}
|
|
@@ -2099,6 +2240,9 @@ async function handleAction(actionNode) {
|
|
|
2099
2240
|
case 'set-settings-section':
|
|
2100
2241
|
setSettingsSection(actionNode.dataset.section);
|
|
2101
2242
|
return;
|
|
2243
|
+
case 'check-app-version':
|
|
2244
|
+
await checkSettingsAppVersion();
|
|
2245
|
+
return;
|
|
2102
2246
|
case 'open-delete-api-token-modal':
|
|
2103
2247
|
openDeleteSettingsApiTokenModal(actionNode.dataset.tokenId);
|
|
2104
2248
|
return;
|
|
@@ -2387,7 +2531,12 @@ async function handleAction(actionNode) {
|
|
|
2387
2531
|
await loadMoreQueryHistory();
|
|
2388
2532
|
return;
|
|
2389
2533
|
case 'open-query-history':
|
|
2390
|
-
if (
|
|
2534
|
+
if (
|
|
2535
|
+
actionNode.dataset.historyId &&
|
|
2536
|
+
openQueryHistoryInEditor(actionNode.dataset.historyId, {
|
|
2537
|
+
notify: getState().route.name !== 'charts',
|
|
2538
|
+
})
|
|
2539
|
+
) {
|
|
2391
2540
|
router.navigate('/editor');
|
|
2392
2541
|
}
|
|
2393
2542
|
return;
|
|
@@ -2420,6 +2569,17 @@ async function handleAction(actionNode) {
|
|
|
2420
2569
|
}
|
|
2421
2570
|
}
|
|
2422
2571
|
return;
|
|
2572
|
+
case 'open-charts-query-detail':
|
|
2573
|
+
if (actionNode.dataset.historyId) {
|
|
2574
|
+
setChartsDetailPanelVisibility(true);
|
|
2575
|
+
if (getState().route.params?.historyId !== actionNode.dataset.historyId) {
|
|
2576
|
+
router.navigate(`/charts/${encodeURIComponent(actionNode.dataset.historyId)}`);
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
return;
|
|
2580
|
+
case 'close-charts-query-detail':
|
|
2581
|
+
setChartsDetailPanelVisibility(false);
|
|
2582
|
+
return;
|
|
2423
2583
|
case 'toggle-charts-sql-panel':
|
|
2424
2584
|
toggleChartsSqlPanel();
|
|
2425
2585
|
return;
|
|
@@ -2982,6 +3142,11 @@ document.addEventListener('input', event => {
|
|
|
2982
3142
|
return;
|
|
2983
3143
|
}
|
|
2984
3144
|
|
|
3145
|
+
if (bindNode.dataset.bind === 'charts-history-search') {
|
|
3146
|
+
setChartsHistorySearchInput(bindNode.value);
|
|
3147
|
+
return;
|
|
3148
|
+
}
|
|
3149
|
+
|
|
2985
3150
|
if (bindNode.dataset.bind === 'query-chart-draft:name') {
|
|
2986
3151
|
updateCurrentQueryChartDraftField('name', bindNode.value);
|
|
2987
3152
|
}
|
|
@@ -1018,6 +1018,20 @@ function getExportOptions() {
|
|
|
1018
1018
|
meta: ".md",
|
|
1019
1019
|
preview: ["| id | name |", "| --- | --- |"],
|
|
1020
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
|
+
},
|
|
1021
1035
|
{
|
|
1022
1036
|
label: "Duplicate",
|
|
1023
1037
|
format: "table",
|
|
@@ -1047,7 +1061,7 @@ function renderTextExportModal(modal, action) {
|
|
|
1047
1061
|
${disabledAttribute}
|
|
1048
1062
|
/>
|
|
1049
1063
|
</label>
|
|
1050
|
-
<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">
|
|
1051
1065
|
${getExportOptions()
|
|
1052
1066
|
.map(
|
|
1053
1067
|
(option) => `
|
|
@@ -1784,6 +1798,8 @@ export function renderModal(state) {
|
|
|
1784
1798
|
modal.kind === "row-update-preview" ||
|
|
1785
1799
|
modal.kind === "table-designer-constraints"
|
|
1786
1800
|
? "max-w-3xl"
|
|
1801
|
+
: modal.kind === "query-export" || modal.kind === "data-export"
|
|
1802
|
+
? "max-w-4xl"
|
|
1787
1803
|
: "max-w-xl"
|
|
1788
1804
|
} border border-outline-variant/20 bg-surface-container shadow-[0_24px_80px_rgba(0,0,0,0.45)]">
|
|
1789
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
|
+
}
|