sqlite-hub 1.4.0 → 1.5.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.
Files changed (40) hide show
  1. package/README.md +9 -0
  2. package/bin/sqlite-hub.js +555 -122
  3. package/docs/API.md +30 -0
  4. package/docs/CLI.md +22 -0
  5. package/docs/CLI_API_PARITY.md +61 -0
  6. package/docs/changelog.md +9 -1
  7. package/docs/guidelines/AGENTS.md +32 -0
  8. package/docs/{DESIGN_GUIDELINES.md → guidelines/DESIGN.md} +10 -0
  9. package/docs/todo.md +1 -13
  10. package/frontend/js/api.js +45 -0
  11. package/frontend/js/app.js +192 -2
  12. package/frontend/js/components/connectionCard.js +3 -1
  13. package/frontend/js/components/modal.js +356 -15
  14. package/frontend/js/components/sidebar.js +41 -7
  15. package/frontend/js/components/topNav.js +2 -14
  16. package/frontend/js/router.js +10 -0
  17. package/frontend/js/store.js +448 -0
  18. package/frontend/js/utils/inputClear.js +36 -0
  19. package/frontend/js/utils/syntheticData.js +240 -0
  20. package/frontend/js/views/backups.js +52 -0
  21. package/frontend/js/views/data.js +16 -0
  22. package/frontend/js/views/logs.js +339 -0
  23. package/frontend/js/views/settings.js +77 -27
  24. package/frontend/js/views/structure.js +6 -0
  25. package/frontend/js/views/tableAdvisor.js +385 -0
  26. package/frontend/js/views/tableDesigner.js +6 -0
  27. package/frontend/styles/components.css +149 -0
  28. package/frontend/styles/tailwind.generated.css +1 -1
  29. package/frontend/styles/views.css +8 -0
  30. package/package.json +1 -1
  31. package/server/routes/data.js +45 -0
  32. package/server/routes/externalApi.js +285 -2
  33. package/server/routes/logs.js +127 -0
  34. package/server/server.js +3 -0
  35. package/server/services/databaseCommandService.js +45 -0
  36. package/server/services/sqlite/dataBrowserService.js +36 -0
  37. package/server/services/sqlite/introspection.js +226 -22
  38. package/server/services/sqlite/syntheticDataGenerator.js +728 -0
  39. package/server/services/sqlite/tableAdvisor.js +790 -0
  40. package/server/services/storage/appStateStore.js +773 -169
@@ -17,6 +17,7 @@ import {
17
17
  MEDIA_TAGGING_DEFAULT_TAG_TABLE,
18
18
  MEDIA_TAGGING_DEFAULT_TAG_TABLE_SQL,
19
19
  } from '../lib/mediaTaggingDefaults.js';
20
+ import { SYNTHETIC_GENERATOR_TYPES, getSyntheticGeneratorLabel } from '../utils/syntheticData.js';
20
21
 
21
22
  function renderField({ label, name, type = 'text', placeholder = '', value = '' }) {
22
23
  return `
@@ -225,7 +226,7 @@ function renderEditConnectionForm(modal) {
225
226
  label: 'Upload image',
226
227
  name: 'logoFile',
227
228
  accept: '.png,.jpg,.jpeg,.webp,image/png,image/jpeg,image/webp',
228
- helpText: 'Allowed formats: PNG, JPG, WEBP. The file is stored in db_logos.',
229
+ helpText: 'Allowed formats: PNG, JPG, WEBP.',
229
230
  })}
230
231
  ${
231
232
  connection.logoUrl
@@ -548,12 +549,13 @@ function renderTypeGenerationCheckbox({ label, field, checked }) {
548
549
 
549
550
  function renderTypeGenerationCodePreview(code) {
550
551
  const text = String(code ?? '');
551
- const normalized = text.includes('\n') || text.includes('\\n')
552
- ? text.replace(/\\n/g, '\n')
553
- : text
554
- .replace(/\{\s*/g, '{\n ')
555
- .replace(/;\s*/g, ';\n ')
556
- .replace(/\s*\}\s*$/g, '\n}');
552
+ const normalized =
553
+ text.includes('\n') || text.includes('\\n')
554
+ ? text.replace(/\\n/g, '\n')
555
+ : text
556
+ .replace(/\{\s*/g, '{\n ')
557
+ .replace(/;\s*/g, ';\n ')
558
+ .replace(/\s*\}\s*$/g, '\n}');
557
559
 
558
560
  return normalized
559
561
  .replace(/\r\n/g, '\n')
@@ -670,7 +672,7 @@ export function renderGenerateTypesForm(modal) {
670
672
  : ''
671
673
  }
672
674
  <pre class="type-generation-code-preview custom-scrollbar border border-outline-variant/10 bg-surface-container-lowest px-4 py-4 font-mono text-xs leading-6 text-on-surface"><code>${renderTypeGenerationCodePreview(
673
- modal.loading ? 'Generating...' : result.code ?? '',
675
+ modal.loading ? 'Generating...' : (result.code ?? ''),
674
676
  )}</code></pre>
675
677
  ${renderError(modal.error)}
676
678
  </div>
@@ -696,6 +698,337 @@ export function renderGenerateTypesForm(modal) {
696
698
  `;
697
699
  }
698
700
 
701
+ function renderSyntheticGeneratorOptions(value) {
702
+ return SYNTHETIC_GENERATOR_TYPES.map(
703
+ type =>
704
+ `<option value="${escapeHtml(type.value)}" ${String(type.value) === String(value) ? 'selected' : ''}>${escapeHtml(
705
+ type.label,
706
+ )}</option>`,
707
+ ).join('');
708
+ }
709
+
710
+ function renderSyntheticOptionInput({
711
+ columnName,
712
+ option,
713
+ type = 'text',
714
+ value = '',
715
+ min = null,
716
+ max = null,
717
+ step = null,
718
+ placeholder = '',
719
+ }) {
720
+ const attributes = [
721
+ `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"`,
722
+ placeholder ? `aria-label="${escapeHtml(placeholder)}"` : '',
723
+ `data-bind="generate-data-mapping"`,
724
+ `data-column-name="${escapeHtml(columnName)}"`,
725
+ `data-field="${escapeHtml(option)}"`,
726
+ placeholder ? `placeholder="${escapeHtml(placeholder)}"` : '',
727
+ `type="${escapeHtml(type)}"`,
728
+ `value="${escapeHtml(value)}"`,
729
+ min !== null ? `min="${escapeHtml(min)}"` : '',
730
+ max !== null ? `max="${escapeHtml(max)}"` : '',
731
+ step !== null ? `step="${escapeHtml(step)}"` : '',
732
+ ].filter(Boolean);
733
+
734
+ return `<input ${attributes.join(' ')} />`;
735
+ }
736
+
737
+ function renderSyntheticOptionSelect({ columnName, option, value = '', options = [] }) {
738
+ return `
739
+ <select
740
+ class="control-select w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
741
+ data-bind="generate-data-mapping"
742
+ data-column-name="${escapeHtml(columnName)}"
743
+ data-field="${escapeHtml(option)}"
744
+ >
745
+ ${options
746
+ .map(
747
+ item =>
748
+ `<option value="${escapeHtml(item.value)}" ${String(item.value) === String(value) ? 'selected' : ''}>${escapeHtml(
749
+ item.label,
750
+ )}</option>`,
751
+ )
752
+ .join('')}
753
+ </select>
754
+ `;
755
+ }
756
+
757
+ function renderSyntheticMappingOptions(mapping) {
758
+ const options = mapping.options ?? {};
759
+ const columnName = mapping.columnName;
760
+
761
+ switch (mapping.generator) {
762
+ case 'static':
763
+ return renderSyntheticOptionInput({
764
+ columnName,
765
+ option: 'value',
766
+ value: options.value ?? '',
767
+ placeholder: 'Value',
768
+ });
769
+ case 'randomInteger':
770
+ return `
771
+ <div class="synthetic-generator-options-grid synthetic-generator-options-grid--two">
772
+ ${renderSyntheticOptionInput({
773
+ columnName,
774
+ option: 'min',
775
+ type: 'number',
776
+ value: options.min ?? 1,
777
+ step: 1,
778
+ placeholder: 'Min',
779
+ })}
780
+ ${renderSyntheticOptionInput({
781
+ columnName,
782
+ option: 'max',
783
+ type: 'number',
784
+ value: options.max ?? 1000,
785
+ step: 1,
786
+ placeholder: 'Max',
787
+ })}
788
+ </div>
789
+ `;
790
+ case 'randomDecimal':
791
+ return `
792
+ <div class="synthetic-generator-options-grid synthetic-generator-options-grid--three">
793
+ ${renderSyntheticOptionInput({
794
+ columnName,
795
+ option: 'min',
796
+ type: 'number',
797
+ value: options.min ?? 0,
798
+ step: '0.01',
799
+ placeholder: 'Min',
800
+ })}
801
+ ${renderSyntheticOptionInput({
802
+ columnName,
803
+ option: 'max',
804
+ type: 'number',
805
+ value: options.max ?? 1000,
806
+ step: '0.01',
807
+ placeholder: 'Max',
808
+ })}
809
+ ${renderSyntheticOptionInput({
810
+ columnName,
811
+ option: 'decimals',
812
+ type: 'number',
813
+ value: options.decimals ?? 2,
814
+ min: 0,
815
+ max: 8,
816
+ step: 1,
817
+ placeholder: 'Decimals',
818
+ })}
819
+ </div>
820
+ `;
821
+ case 'boolean':
822
+ return renderSyntheticOptionInput({
823
+ columnName,
824
+ option: 'trueProbability',
825
+ type: 'number',
826
+ value: options.trueProbability ?? 50,
827
+ min: 0,
828
+ max: 100,
829
+ step: 1,
830
+ placeholder: 'True %',
831
+ });
832
+ case 'timestamp':
833
+ return `
834
+ <div class="synthetic-generator-options-grid synthetic-generator-options-grid--timestamp">
835
+ ${renderSyntheticOptionSelect({
836
+ columnName,
837
+ option: 'range',
838
+ value: options.range ?? 'last30',
839
+ options: [
840
+ { value: 'last30', label: 'Last 30 days' },
841
+ { value: 'last365', label: 'Last 365 days' },
842
+ { value: 'custom', label: 'Custom' },
843
+ ],
844
+ })}
845
+ ${
846
+ options.range === 'custom'
847
+ ? `${renderSyntheticOptionInput({
848
+ columnName,
849
+ option: 'from',
850
+ type: 'datetime-local',
851
+ value: options.from ?? '',
852
+ placeholder: 'From',
853
+ })}
854
+ ${renderSyntheticOptionInput({
855
+ columnName,
856
+ option: 'to',
857
+ type: 'datetime-local',
858
+ value: options.to ?? '',
859
+ placeholder: 'To',
860
+ })}`
861
+ : ''
862
+ }
863
+ </div>
864
+ `;
865
+ case 'oneOf':
866
+ return renderSyntheticOptionInput({
867
+ columnName,
868
+ option: 'values',
869
+ value: options.values ?? '',
870
+ placeholder: 'Comma values',
871
+ });
872
+ case 'skip':
873
+ default:
874
+ return `<span class="font-mono text-[10px] uppercase tracking-[0.16em] text-on-surface-variant/45">No options</span>`;
875
+ }
876
+ }
877
+
878
+ function formatSyntheticPreviewValue(value) {
879
+ if (value === null || value === undefined) {
880
+ return 'NULL';
881
+ }
882
+
883
+ if (typeof value === 'object') {
884
+ return JSON.stringify(value);
885
+ }
886
+
887
+ return String(value);
888
+ }
889
+
890
+ function renderSyntheticPreview(modal) {
891
+ const rows = (modal.previewRows ?? []).slice(0, 3);
892
+ const columns = modal.previewColumns?.length
893
+ ? modal.previewColumns
894
+ : (modal.columns ?? []).map(column => column.name);
895
+
896
+ if (!rows.length) {
897
+ return `
898
+ <div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-4 text-sm text-on-surface-variant/55">
899
+ Preview creates up to 3 rows without writing to the database.
900
+ </div>
901
+ `;
902
+ }
903
+
904
+ return `
905
+ <div class="synthetic-generator-preview">
906
+ <table>
907
+ <thead>
908
+ <tr>
909
+ ${columns.map(column => `<th>${escapeHtml(column)}</th>`).join('')}
910
+ </tr>
911
+ </thead>
912
+ <tbody>
913
+ ${rows
914
+ .map(
915
+ row => `
916
+ <tr>
917
+ ${columns
918
+ .map(
919
+ column =>
920
+ `<td title="${escapeHtml(formatSyntheticPreviewValue(row[column]))}">${escapeHtml(
921
+ truncateMiddle(formatSyntheticPreviewValue(row[column]), 56),
922
+ )}</td>`,
923
+ )
924
+ .join('')}
925
+ </tr>
926
+ `,
927
+ )
928
+ .join('')}
929
+ </tbody>
930
+ </table>
931
+ </div>
932
+ `;
933
+ }
934
+
935
+ export function renderGenerateDataForm(modal) {
936
+ const columns = modal.columns ?? [];
937
+ const mappings = modal.mappings ?? [];
938
+ const disabledAttribute = modal.submitting || modal.previewLoading ? 'disabled aria-disabled="true"' : '';
939
+
940
+ return `
941
+ <form class="generate-data-modal-form" data-form="generate-data">
942
+ <div class="synthetic-generator-count-row">
943
+ <label class="block space-y-2">
944
+ <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">Rows</span>
945
+ <input
946
+ 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"
947
+ data-bind="generate-data-field"
948
+ data-field="rowCount"
949
+ max="10000"
950
+ min="1"
951
+ name="rowCount"
952
+ step="1"
953
+ type="number"
954
+ value="${escapeHtml(modal.rowCount ?? 100)}"
955
+ />
956
+ </label>
957
+ <div class="flex items-end text-sm leading-6 text-on-surface-variant/60">
958
+ Create test rows for "${escapeHtml(modal.tableName ?? '')}".
959
+ </div>
960
+ </div>
961
+ <div class="synthetic-generator-grid custom-scrollbar">
962
+ <div class="synthetic-generator-grid__header">
963
+ <div>Column</div>
964
+ <div>SQLite Type</div>
965
+ <div>Generator</div>
966
+ <div>Options</div>
967
+ </div>
968
+ ${
969
+ columns.length
970
+ ? mappings
971
+ .map(mapping => {
972
+ const column = columns.find(item => item.name === mapping.columnName) ?? {};
973
+ const sqliteType = column.declaredType || column.affinity || 'ANY';
974
+
975
+ return `
976
+ <div class="synthetic-generator-grid__row">
977
+ <div class="min-w-0">
978
+ <div class="synthetic-generator-column-name">${escapeHtml(mapping.columnName)}</div>
979
+ ${
980
+ mapping.note
981
+ ? `<div class="synthetic-generator-column-note">${escapeHtml(mapping.note)}</div>`
982
+ : ''
983
+ }
984
+ </div>
985
+ <div class="synthetic-generator-sqlite-type">
986
+ ${escapeHtml(sqliteType)}
987
+ ${column.notNull ? '<span>NOT NULL</span>' : ''}
988
+ </div>
989
+ <div>
990
+ <select
991
+ class="control-select w-full border border-outline-variant/20 bg-surface-container-lowest text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
992
+ data-bind="generate-data-mapping"
993
+ data-column-name="${escapeHtml(mapping.columnName)}"
994
+ data-field="generator"
995
+ title="${escapeHtml(getSyntheticGeneratorLabel(mapping.generator))}"
996
+ >
997
+ ${renderSyntheticGeneratorOptions(mapping.generator)}
998
+ </select>
999
+ </div>
1000
+ <div class="min-w-0">
1001
+ ${renderSyntheticMappingOptions(mapping)}
1002
+ </div>
1003
+ </div>
1004
+ `;
1005
+ })
1006
+ .join('')
1007
+ : `<div class="px-4 py-6 text-sm text-on-surface-variant/55">No writable columns available.</div>`
1008
+ }
1009
+ </div>
1010
+ <div class="space-y-3">
1011
+ <div class="font-mono text-[10px] uppercase tracking-[0.18em] text-primary-container">Preview</div>
1012
+ ${renderSyntheticPreview(modal)}
1013
+ </div>
1014
+ ${renderError(modal.error)}
1015
+ <div class="flex items-center justify-between gap-3 pt-2">
1016
+ <button class="standard-button" data-action="close-modal" type="button">Cancel</button>
1017
+ <div class="flex flex-wrap items-center justify-end gap-2">
1018
+ <button class="standard-button" data-action="preview-generate-data" type="button" ${disabledAttribute}>
1019
+ <span class="material-symbols-outlined text-sm">visibility</span>
1020
+ ${modal.previewLoading ? 'Previewing...' : 'Preview'}
1021
+ </button>
1022
+ <button class="signature-button" type="submit" ${modal.submitting ? 'disabled aria-disabled="true"' : ''}>
1023
+ <span class="material-symbols-outlined text-sm">auto_awesome</span>
1024
+ ${modal.submitting ? 'Inserting...' : 'Insert Rows'}
1025
+ </button>
1026
+ </div>
1027
+ </div>
1028
+ </form>
1029
+ `;
1030
+ }
1031
+
699
1032
  function renderDeleteRowConfirmForm(modal) {
700
1033
  const rowPreview = modal.rowPreview ?? [];
701
1034
  const rowLabelMarkup = modal.rowLabel
@@ -2005,6 +2338,11 @@ export function renderModal(state) {
2005
2338
  title: 'Generate Types',
2006
2339
  body: renderGenerateTypesForm(modal),
2007
2340
  },
2341
+ 'generate-data': {
2342
+ eyebrow: 'Data Browser // Synthetic rows',
2343
+ title: 'Generate Synthetic Data',
2344
+ body: renderGenerateDataForm(modal),
2345
+ },
2008
2346
  'edit-connection': {
2009
2347
  eyebrow: 'Registry // Update saved SQLite target',
2010
2348
  title: 'Edit Connection',
@@ -2093,6 +2431,11 @@ export function renderModal(state) {
2093
2431
  return '';
2094
2432
  }
2095
2433
 
2434
+ const modalBodyClass =
2435
+ modal.kind === 'generate-data'
2436
+ ? 'app-modal-body app-modal-body--no-scroll px-6 py-6'
2437
+ : 'app-modal-body custom-scrollbar space-y-5 px-6 py-6';
2438
+
2096
2439
  return `
2097
2440
  <div class="fixed inset-0 z-50 flex items-center justify-center bg-background/85 px-4 backdrop-blur-sm">
2098
2441
  <div class="app-modal-shell w-full ${
@@ -2102,13 +2445,11 @@ export function renderModal(state) {
2102
2445
  modal.kind === 'row-update-preview' ||
2103
2446
  modal.kind === 'table-designer-constraints'
2104
2447
  ? 'max-w-3xl'
2105
- : modal.kind === 'generate-types'
2448
+ : modal.kind === 'generate-types' || modal.kind === 'generate-data'
2106
2449
  ? 'max-w-6xl'
2107
- : modal.kind === 'query-export' ||
2108
- modal.kind === 'data-export' ||
2109
- modal.kind === 'backup-safety'
2110
- ? 'max-w-4xl'
2111
- : 'max-w-xl'
2450
+ : modal.kind === 'query-export' || modal.kind === 'data-export' || modal.kind === 'backup-safety'
2451
+ ? 'max-w-4xl'
2452
+ : 'max-w-xl'
2112
2453
  } border border-outline-variant/20 bg-surface-container shadow-[0_24px_80px_rgba(0,0,0,0.45)]">
2113
2454
  <div class="shrink-0 flex items-start justify-between gap-4 border-b border-outline-variant/10 bg-surface-container-low px-6 py-5">
2114
2455
  <div>
@@ -2127,7 +2468,7 @@ export function renderModal(state) {
2127
2468
  <span class="material-symbols-outlined">close</span>
2128
2469
  </button>
2129
2470
  </div>
2130
- <div class="app-modal-body custom-scrollbar space-y-5 px-6 py-6">${config.body}</div>
2471
+ <div class="${modalBodyClass}">${config.body}</div>
2131
2472
  </div>
2132
2473
  </div>
2133
2474
  `;
@@ -3,13 +3,36 @@ import { renderConnectionLogo } from './connectionLogo.js';
3
3
 
4
4
  const sidebarItems = [
5
5
  { label: 'Connections', href: '#/connections', key: 'connections', icon: 'database' },
6
- { label: 'Overview', href: '#/overview', key: 'overview', icon: 'dashboard' },
7
6
  { label: 'Data', href: '#/data', key: 'data', icon: 'table_rows' },
8
- { label: 'Structure', href: '#/structure', key: 'structure', icon: 'account_tree' },
9
7
  { label: 'SQL_Editor', href: '#/editor', key: 'editor', icon: 'terminal' },
10
- { label: 'Charts', href: '#/charts', key: 'charts', icon: 'bar_chart' },
11
- { label: 'Documents', href: '#/documents', key: 'documents', icon: 'description' },
12
- { label: 'Table_Designer', href: '#/table-designer', key: 'tableDesigner', icon: 'table_chart' },
8
+ {
9
+ label: 'SCHEMA',
10
+ key: 'schema',
11
+ icon: 'account_tree',
12
+ children: [
13
+ { label: 'STRUCTURE', href: '#/structure', key: 'structure' },
14
+ { label: 'ADVISOR', href: '#/table-advisor', key: 'tableAdvisor' },
15
+ { label: 'DESIGNER', href: '#/table-designer', key: 'tableDesigner' },
16
+ ],
17
+ },
18
+ {
19
+ label: 'Insights',
20
+ key: 'insights',
21
+ icon: 'monitoring',
22
+ children: [
23
+ { label: 'CHARTS', href: '#/charts', key: 'charts' },
24
+ { label: 'OVERVIEW', href: '#/overview', key: 'overview' },
25
+ ],
26
+ },
27
+ {
28
+ label: 'Workspace',
29
+ key: 'workspace',
30
+ icon: 'folder_open',
31
+ children: [
32
+ { label: 'DOCUMENTS', href: '#/documents', key: 'documents' },
33
+ { label: 'BACKUPS', href: '#/backups', key: 'backups' },
34
+ ],
35
+ },
13
36
  {
14
37
  label: 'MEDIA_TAGGING',
15
38
  key: 'mediaTagging',
@@ -19,7 +42,6 @@ const sidebarItems = [
19
42
  { label: 'TAGGING_QUEUE', href: '#/media-tagging/queue', key: 'mediaTaggingQueue' },
20
43
  ],
21
44
  },
22
- { label: 'Backups', href: '#/backups', key: 'backups', icon: 'inventory_2' },
23
45
  { label: 'Settings', href: '#/settings', key: 'settings', icon: 'settings' },
24
46
  ];
25
47
 
@@ -36,6 +58,18 @@ function getActiveSidebarKey(routeName) {
36
58
  return 'mediaTagging';
37
59
  }
38
60
 
61
+ if (routeName === 'structure' || routeName === 'tableAdvisor' || routeName === 'tableDesigner') {
62
+ return 'schema';
63
+ }
64
+
65
+ if (routeName === 'charts' || routeName === 'overview') {
66
+ return 'insights';
67
+ }
68
+
69
+ if (routeName === 'documents' || routeName === 'backups') {
70
+ return 'workspace';
71
+ }
72
+
39
73
  return routeName;
40
74
  }
41
75
 
@@ -140,7 +174,7 @@ function renderConnectionQuickPicker(state) {
140
174
 
141
175
  export function renderSidebar(state) {
142
176
  const activeKey = getActiveSidebarKey(state.route.name);
143
- const expandedKey = activeKey === 'mediaTagging' ? 'mediaTagging' : null;
177
+ const expandedKey = sidebarItems.some(item => item.key === activeKey && item.children) ? activeKey : null;
144
178
 
145
179
  return `
146
180
  <nav class="sidebar-links">
@@ -3,20 +3,8 @@ export function renderTopNav() {
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
7
- class="top-nav-icon"
8
- data-action="open-modal"
9
- data-modal="open-connection"
10
- type="button"
11
- aria-label="Open Database"
12
- >
13
- <span class="material-symbols-outlined">folder_open</span>
14
- </button>
15
- <button class="top-nav-icon" data-action="navigate" data-to="/editor" type="button" aria-label="SQL Editor">
16
- <span class="material-symbols-outlined">terminal</span>
17
- </button>
18
- <button class="top-nav-icon" data-action="navigate" data-to="/settings" type="button" aria-label="Settings">
19
- <span class="material-symbols-outlined">settings</span>
6
+ <button class="top-nav-icon" data-action="navigate" data-to="/logs" type="button" aria-label="Logs">
7
+ <span class="material-symbols-outlined">receipt_long</span>
20
8
  </button>
21
9
  </div>
22
10
  </div>
@@ -84,6 +84,14 @@ export function parseHash(hash = window.location.hash) {
84
84
  tableName: segments[1] && segments[1] !== 'new' ? decodeRouteValue(segments[1]) : null,
85
85
  },
86
86
  };
87
+ case 'table-advisor':
88
+ return {
89
+ name: 'tableAdvisor',
90
+ path: cleanPath,
91
+ params: {
92
+ tableName: segments[1] ? decodeRouteValue(segments[1]) : null,
93
+ },
94
+ };
87
95
  case 'media-tagging':
88
96
  if (segments[1] === 'queue') {
89
97
  return { name: 'mediaTaggingQueue', path: '/media-tagging/queue', params: {} };
@@ -91,6 +99,8 @@ export function parseHash(hash = window.location.hash) {
91
99
  return { name: 'mediaTaggingSetup', path: '/media-tagging', params: {} };
92
100
  case 'settings':
93
101
  return { name: 'settings', path: '/settings', params: {} };
102
+ case 'logs':
103
+ return { name: 'logs', path: '/logs', params: {} };
94
104
  default:
95
105
  return { name: 'notFound', path: cleanPath, params: {} };
96
106
  }