sqlite-hub 1.2.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.
- package/README.md +113 -71
- package/bin/sqlite-hub.js +723 -116
- package/docs/API.md +152 -0
- package/docs/CLI.md +249 -0
- package/docs/CLI_API_PARITY.md +61 -0
- package/docs/changelog.md +149 -0
- package/docs/guidelines/AGENTS.md +32 -0
- package/docs/guidelines/DESIGN.md +55 -0
- package/docs/shortkeys.md +27 -0
- package/docs/todo.md +4 -0
- package/examples/api/generate-types.js +52 -0
- package/frontend/assets/mockups/backups_1_1920.webp +0 -0
- package/frontend/assets/mockups/backups_2_create_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_3_edit_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_4_restore_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_5_delete_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_1_1920.webp +0 -0
- package/frontend/assets/mockups/charts_2_query_detail_1920.webp +0 -0
- package/frontend/assets/mockups/charts_3_create_query_chart_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_4_edit_query_chart_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_5_delete_query_chart_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_6_copy_column_modal_1920.webp +0 -0
- package/frontend/assets/mockups/connections_1_1920.webp +0 -0
- package/frontend/assets/mockups/connections_2_create_connection_modal_1920.webp +0 -0
- package/frontend/assets/mockups/connections_3_open_connection_modal_1920.webp +0 -0
- package/frontend/assets/mockups/data_1_1920.webp +0 -0
- package/frontend/assets/mockups/data_2_roweditor_1920.webp +0 -0
- package/frontend/assets/mockups/data_3_data_export_modal_1920.webp +0 -0
- package/frontend/assets/mockups/documents_1_1920.webp +0 -0
- package/frontend/assets/mockups/documents_2_document_insert_table_modal_1920.webp +0 -0
- package/frontend/assets/mockups/documents_3_document_insert_note_modal_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_queue_1_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_setup_1_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_setup_2_create_media_tagging_tag_table_modal_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_setup_3_create_media_tagging_mapping_table_modal_1920.webp +0 -0
- package/frontend/assets/mockups/overview_1_1920.webp +0 -0
- package/frontend/assets/mockups/settings_1_1920.webp +0 -0
- package/frontend/assets/mockups/sql_editor_1_1920.webp +0 -0
- package/frontend/assets/mockups/sql_editor_2_query_detail_1920.webp +0 -0
- package/frontend/assets/mockups/sql_editor_3_query_export_modal_1920.webp +0 -0
- package/frontend/assets/mockups/structure_1_1920.webp +0 -0
- package/frontend/assets/mockups/structure_2_generate_types_modal_1920.webp +0 -0
- package/frontend/assets/mockups/structure_3_generate_types_modal_1920.webp +0 -0
- package/frontend/assets/mockups/table_designer_1_1920.webp +0 -0
- package/frontend/assets/mockups/table_designer_2_table_designer_constraints_modal_1920.webp +0 -0
- package/frontend/js/api.js +63 -0
- package/frontend/js/app.js +310 -10
- package/frontend/js/components/connectionCard.js +4 -2
- package/frontend/js/components/emptyState.js +4 -4
- package/frontend/js/components/generateTypesDropdown.js +33 -0
- package/frontend/js/components/metricCard.js +1 -1
- package/frontend/js/components/modal.js +555 -21
- package/frontend/js/components/pageHeader.js +1 -1
- package/frontend/js/components/queryHistoryDetail.js +3 -3
- package/frontend/js/components/queryHistoryHeader.js +1 -1
- package/frontend/js/components/queryHistoryPanel.js +1 -1
- package/frontend/js/components/rowEditorPanel.js +13 -3
- package/frontend/js/components/sidebar.js +41 -7
- package/frontend/js/components/topNav.js +2 -14
- package/frontend/js/router.js +38 -6
- package/frontend/js/store.js +870 -8
- package/frontend/js/utils/inputClear.js +36 -0
- package/frontend/js/utils/syntheticData.js +240 -0
- package/frontend/js/views/backups.js +581 -37
- package/frontend/js/views/charts.js +9 -9
- package/frontend/js/views/connections.js +2 -2
- package/frontend/js/views/data.js +17 -1
- package/frontend/js/views/documents.js +3 -3
- package/frontend/js/views/editor.js +6 -6
- package/frontend/js/views/logs.js +339 -0
- package/frontend/js/views/mediaTagging.js +7 -5
- package/frontend/js/views/overview.js +3 -3
- package/frontend/js/views/settings.js +80 -30
- package/frontend/js/views/structure.js +12 -1
- package/frontend/js/views/tableAdvisor.js +385 -0
- package/frontend/js/views/tableDesigner.js +6 -0
- package/frontend/styles/base.css +1 -40
- package/frontend/styles/components.css +258 -232
- package/frontend/styles/structure-graph.css +32 -68
- package/frontend/styles/tailwind.css +8 -4
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/tokens.css +28 -21
- package/frontend/styles/views.css +102 -261
- package/package.json +18 -2
- package/server/routes/backups.js +18 -2
- package/server/routes/data.js +45 -0
- package/server/routes/externalApi.js +321 -2
- package/server/routes/logs.js +127 -0
- package/server/routes/structure.js +22 -0
- package/server/server.js +3 -0
- package/server/services/databaseCommandService.js +59 -0
- package/server/services/sqlite/backupDiff.js +914 -0
- package/server/services/sqlite/backupService.js +77 -1
- package/server/services/sqlite/dataBrowserService.js +36 -0
- package/server/services/sqlite/introspection.js +226 -22
- package/server/services/sqlite/structureService.js +7 -0
- package/server/services/sqlite/syntheticDataGenerator.js +728 -0
- package/server/services/sqlite/tableAdvisor.js +790 -0
- package/server/services/storage/appStateStore.js +773 -169
- package/server/services/typeGenerationService.js +663 -0
- package/tailwind.config.cjs +0 -1
- package/.github/funding.yml +0 -2
- package/.github/workflows/ci.yml +0 -36
- package/database.sqlite +0 -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/tests/api-token-auth.test.js +0 -279
- package/tests/backup-manager.test.js +0 -140
- package/tests/backups-view.test.js +0 -70
- package/tests/charts-height-preset-storage.test.js +0 -60
- package/tests/charts-route-state.test.js +0 -144
- package/tests/check-constraint-options.test.js +0 -90
- package/tests/cli-args.test.js +0 -113
- package/tests/cli-service-delegation.test.js +0 -140
- package/tests/connection-removal.test.js +0 -52
- package/tests/connections-file-dialog-route.test.js +0 -89
- package/tests/copy-column-modal.test.js +0 -131
- package/tests/database-command-service.test.js +0 -174
- package/tests/database-documents.test.js +0 -85
- package/tests/documents-view.test.js +0 -132
- package/tests/dropdown-button.test.js +0 -101
- package/tests/email-preview.test.js +0 -89
- package/tests/export-blob.test.js +0 -99
- package/tests/export-filenames.test.js +0 -38
- package/tests/file-path-preview.test.js +0 -165
- package/tests/form-controls.test.js +0 -34
- package/tests/json-preview.test.js +0 -49
- package/tests/local-request-security.test.js +0 -85
- package/tests/markdown-documents.test.js +0 -103
- package/tests/native-file-dialog.test.js +0 -105
- package/tests/query-editor.test.js +0 -28
- package/tests/query-history-detail.test.js +0 -37
- package/tests/query-history-header.test.js +0 -30
- package/tests/query-results-truncation.test.js +0 -20
- package/tests/risky-sql.test.js +0 -30
- package/tests/row-editor-json.test.js +0 -82
- package/tests/row-editor-null-values.test.js +0 -184
- package/tests/row-editor-timestamp-preview.test.js +0 -192
- package/tests/security-paths.test.js +0 -84
- package/tests/settings-api-tokens-route.test.js +0 -97
- package/tests/settings-metadata.test.js +0 -114
- package/tests/settings-view.test.js +0 -108
- package/tests/sql-formatter.test.js +0 -173
- package/tests/sql-highlight.test.js +0 -38
- package/tests/sql-identifier-safety.test.js +0 -171
- package/tests/sql-result-limit.test.js +0 -66
- package/tests/structure-view.test.js +0 -60
- package/tests/table-designer-v2-unique-constraints.test.js +0 -78
- package/tests/table-scroll-state.test.js +0 -70
- package/tests/text-cell-stats.test.js +0 -38
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { renderPageHeader } from '../components/pageHeader.js';
|
|
2
2
|
import { renderTextInput } from '../components/formControls.js';
|
|
3
|
-
import { escapeHtml } from '../utils/format.js';
|
|
3
|
+
import { escapeHtml, formatCompactDateTime, formatNumber } from '../utils/format.js';
|
|
4
4
|
|
|
5
5
|
function renderSettingsNavigation(activeSection) {
|
|
6
6
|
const items = [
|
|
@@ -90,6 +90,81 @@ function renderVersionCheckStatus(settings) {
|
|
|
90
90
|
`;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
function renderApiTokenRow(token, tokenSaving) {
|
|
94
|
+
const callCount = Number(token.callCount ?? 0);
|
|
95
|
+
const createdAt = token.createdAt ? formatCompactDateTime(token.createdAt) : 'Unknown';
|
|
96
|
+
const lastCallAt = token.lastCallAt ? formatCompactDateTime(token.lastCallAt) : 'Never';
|
|
97
|
+
|
|
98
|
+
return `
|
|
99
|
+
<div class="grid grid-cols-1 gap-4 border-t border-outline-variant/10 bg-surface-container-high px-4 py-4 xl:grid-cols-[minmax(0,1.35fr)_minmax(8rem,0.75fr)_7rem_minmax(8rem,0.85fr)_8rem] xl:items-center">
|
|
100
|
+
<div class="min-w-0">
|
|
101
|
+
<div class="truncate text-sm font-semibold text-on-surface" title="${escapeHtml(token.name)}">
|
|
102
|
+
${escapeHtml(token.name)}
|
|
103
|
+
</div>
|
|
104
|
+
<div
|
|
105
|
+
class="mt-2 inline-flex max-w-full border border-outline-variant/10 bg-surface-container-lowest px-3 py-1 font-mono text-[11px] text-primary-container"
|
|
106
|
+
title="${escapeHtml(token.tokenPrefix ?? '')}"
|
|
107
|
+
>
|
|
108
|
+
<span class="truncate">${escapeHtml(token.tokenPrefix)}...</span>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
<div>
|
|
112
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/45 xl:hidden">Created</div>
|
|
113
|
+
<div class="mt-1 font-mono text-[11px] text-on-surface-variant/75 xl:mt-0" title="${escapeHtml(token.createdAt ?? 'unknown')}">
|
|
114
|
+
${escapeHtml(createdAt)}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
<div>
|
|
118
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/45 xl:hidden">Calls</div>
|
|
119
|
+
<div class="mt-1 font-mono text-lg font-bold text-on-surface xl:mt-0">
|
|
120
|
+
${escapeHtml(formatNumber(callCount))}
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
<div>
|
|
124
|
+
<div class="text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/45 xl:hidden">Last Call</div>
|
|
125
|
+
<div class="mt-1 font-mono text-[11px] text-on-surface-variant/75 xl:mt-0" title="${escapeHtml(token.lastCallAt ?? 'never')}">
|
|
126
|
+
${escapeHtml(lastCallAt)}
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
<div class="flex xl:justify-end">
|
|
130
|
+
<button
|
|
131
|
+
class="delete-button"
|
|
132
|
+
data-action="open-delete-api-token-modal"
|
|
133
|
+
data-token-id="${escapeHtml(token.id)}"
|
|
134
|
+
type="button"
|
|
135
|
+
${tokenSaving ? 'disabled' : ''}
|
|
136
|
+
>
|
|
137
|
+
<span class="material-symbols-outlined text-sm">delete</span>
|
|
138
|
+
Delete
|
|
139
|
+
</button>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function renderApiTokenRows(apiTokens, tokenSaving) {
|
|
146
|
+
if (!apiTokens.length) {
|
|
147
|
+
return `
|
|
148
|
+
<div class="border border-outline-variant/10 bg-surface-container-high px-4 py-5 text-sm text-on-surface-variant">
|
|
149
|
+
No API tokens exist for this database.
|
|
150
|
+
</div>
|
|
151
|
+
`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return `
|
|
155
|
+
<div class="overflow-hidden border border-outline-variant/10">
|
|
156
|
+
<div class="hidden bg-surface-container-highest px-4 py-3 font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant/55 xl:grid xl:grid-cols-[minmax(0,1.35fr)_minmax(8rem,0.75fr)_7rem_minmax(8rem,0.85fr)_8rem]">
|
|
157
|
+
<div>Token</div>
|
|
158
|
+
<div>Created</div>
|
|
159
|
+
<div>Calls</div>
|
|
160
|
+
<div>Last Call</div>
|
|
161
|
+
<div class="text-right">Actions</div>
|
|
162
|
+
</div>
|
|
163
|
+
${apiTokens.map(token => renderApiTokenRow(token, tokenSaving)).join('')}
|
|
164
|
+
</div>
|
|
165
|
+
`;
|
|
166
|
+
}
|
|
167
|
+
|
|
93
168
|
function renderSettingsContent(state) {
|
|
94
169
|
if (state.settings.loading && !state.settings.appVersion) {
|
|
95
170
|
return `
|
|
@@ -105,7 +180,7 @@ function renderSettingsContent(state) {
|
|
|
105
180
|
if (state.settings.error) {
|
|
106
181
|
return `
|
|
107
182
|
<div class="border border-error/20 bg-error-container/10 px-6 py-5 text-sm text-on-surface">
|
|
108
|
-
<div class="font-
|
|
183
|
+
<div class="font-body text-xs font-bold uppercase tracking-[0.18em] text-error">
|
|
109
184
|
${escapeHtml(state.settings.error.code)}
|
|
110
185
|
</div>
|
|
111
186
|
<div class="mt-2">${escapeHtml(state.settings.error.message)}</div>
|
|
@@ -121,32 +196,7 @@ function renderSettingsContent(state) {
|
|
|
121
196
|
const apiTokens = state.settings.apiTokens ?? [];
|
|
122
197
|
const createdApiToken = state.settings.createdApiToken;
|
|
123
198
|
const tokenSaving = Boolean(state.settings.tokenSaving);
|
|
124
|
-
const tokenItems = apiTokens
|
|
125
|
-
? apiTokens
|
|
126
|
-
.map(
|
|
127
|
-
token => `
|
|
128
|
-
<div class="flex flex-col gap-3 border border-outline-variant/10 bg-surface-container-high px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
|
|
129
|
-
<div class="min-w-0">
|
|
130
|
-
<div class="truncate text-sm font-semibold text-on-surface">${escapeHtml(token.name)}</div>
|
|
131
|
-
<div class="mt-1 font-mono text-[10px] text-on-surface-variant/60">
|
|
132
|
-
${escapeHtml(token.tokenPrefix)}... · Created ${escapeHtml(token.createdAt ?? 'unknown')}
|
|
133
|
-
</div>
|
|
134
|
-
</div>
|
|
135
|
-
<button
|
|
136
|
-
class="delete-button"
|
|
137
|
-
data-action="open-delete-api-token-modal"
|
|
138
|
-
data-token-id="${escapeHtml(token.id)}"
|
|
139
|
-
type="button"
|
|
140
|
-
${tokenSaving ? 'disabled' : ''}
|
|
141
|
-
>
|
|
142
|
-
<span class="material-symbols-outlined text-sm">delete</span>
|
|
143
|
-
Delete
|
|
144
|
-
</button>
|
|
145
|
-
</div>
|
|
146
|
-
`,
|
|
147
|
-
)
|
|
148
|
-
.join('')
|
|
149
|
-
: '<div class="text-sm text-on-surface-variant">No API tokens exist for this database.</div>';
|
|
199
|
+
const tokenItems = renderApiTokenRows(apiTokens, tokenSaving);
|
|
150
200
|
const tokenSection = tokenDatabase
|
|
151
201
|
? `
|
|
152
202
|
<div class="space-y-5">
|
|
@@ -217,7 +267,7 @@ function renderSettingsContent(state) {
|
|
|
217
267
|
<div class="text-[10px] font-mono uppercase tracking-[0.2em] text-on-surface-variant/60">
|
|
218
268
|
Current_Version
|
|
219
269
|
</div>
|
|
220
|
-
<div class="mt-2 font-
|
|
270
|
+
<div class="mt-2 font-body text-4xl font-black uppercase tracking-tight text-primary-container">
|
|
221
271
|
v${appVersion}
|
|
222
272
|
</div>
|
|
223
273
|
</div>
|
|
@@ -225,7 +275,7 @@ function renderSettingsContent(state) {
|
|
|
225
275
|
<div class="text-[10px] font-mono uppercase tracking-[0.2em] text-on-surface-variant/60">
|
|
226
276
|
SQLite_Runtime
|
|
227
277
|
</div>
|
|
228
|
-
<div class="mt-2 font-
|
|
278
|
+
<div class="mt-2 font-body text-4xl font-black uppercase tracking-tight text-primary-container">
|
|
229
279
|
${sqliteVersion}
|
|
230
280
|
</div>
|
|
231
281
|
</div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { clearInspector, renderDdlSection, renderInspector } from '../components/structureGraph.js';
|
|
2
2
|
import { renderDropdownButton } from '../components/dropdownButton.js';
|
|
3
|
+
import { renderGenerateTypesDropdown } from '../components/generateTypesDropdown.js';
|
|
3
4
|
import { renderWorkspaceOpenDropdown } from '../components/workspaceOpenDropdown.js';
|
|
4
5
|
import { escapeHtml, formatNumber } from '../utils/format.js';
|
|
5
6
|
|
|
@@ -247,11 +248,21 @@ function renderGraphSurface(structure, selectedName, detail, detailLoading, tabl
|
|
|
247
248
|
label: 'Table Designer',
|
|
248
249
|
target: tableName => `/table-designer/${encodeURIComponent(tableName)}`,
|
|
249
250
|
},
|
|
251
|
+
{
|
|
252
|
+
icon: 'troubleshoot',
|
|
253
|
+
key: 'table-advisor',
|
|
254
|
+
label: 'Table Advisor',
|
|
255
|
+
target: tableName => `/table-advisor/${encodeURIComponent(tableName)}`,
|
|
256
|
+
},
|
|
250
257
|
{
|
|
251
258
|
key: 'sql-editor',
|
|
252
259
|
},
|
|
253
260
|
],
|
|
254
261
|
})}
|
|
262
|
+
${renderGenerateTypesDropdown({
|
|
263
|
+
selectedTableName: selectedGraphTable?.name ?? '',
|
|
264
|
+
hasTables: Boolean(graph.tables?.length),
|
|
265
|
+
})}
|
|
255
266
|
${renderDropdownButton({
|
|
256
267
|
icon: 'format_shapes',
|
|
257
268
|
label: 'Format',
|
|
@@ -394,7 +405,7 @@ export function renderStructureView(state) {
|
|
|
394
405
|
: state.structure.error
|
|
395
406
|
? `
|
|
396
407
|
<div class="min-h-0 flex-1 border-t border-error/20 bg-error-container/10 px-6 py-5 text-sm text-on-surface">
|
|
397
|
-
<div class="font-
|
|
408
|
+
<div class="font-body text-xs font-bold uppercase tracking-[0.18em] text-error">
|
|
398
409
|
${escapeHtml(state.structure.error.code)}
|
|
399
410
|
</div>
|
|
400
411
|
<div class="mt-2">${escapeHtml(state.structure.error.message)}</div>
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import {
|
|
2
|
+
escapeHtml,
|
|
3
|
+
formatDateTime,
|
|
4
|
+
formatNumber,
|
|
5
|
+
highlightSql,
|
|
6
|
+
truncateMiddle,
|
|
7
|
+
} from '../utils/format.js';
|
|
8
|
+
import { renderWorkspaceOpenDropdown } from '../components/workspaceOpenDropdown.js';
|
|
9
|
+
|
|
10
|
+
const CATEGORY_LABELS = {
|
|
11
|
+
schema: 'Schema',
|
|
12
|
+
constraints: 'Constraints',
|
|
13
|
+
performance: 'Performance',
|
|
14
|
+
'data-quality': 'Data Quality',
|
|
15
|
+
documentation: 'Documentation',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const CATEGORY_ORDER = ['schema', 'constraints', 'performance', 'data-quality', 'documentation'];
|
|
19
|
+
|
|
20
|
+
function getScoreClass(score) {
|
|
21
|
+
if (score >= 80) {
|
|
22
|
+
return 'text-primary-container';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (score >= 60) {
|
|
26
|
+
return 'text-on-surface';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return 'text-error';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function renderError(error) {
|
|
33
|
+
if (!error) {
|
|
34
|
+
return '';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return `
|
|
38
|
+
<div class="border border-error/20 bg-error-container/10 px-6 py-5 text-sm text-on-surface">
|
|
39
|
+
<div class="font-body text-xs font-bold uppercase tracking-[0.18em] text-error">
|
|
40
|
+
${escapeHtml(error.code ?? 'REQUEST_FAILED')}
|
|
41
|
+
</div>
|
|
42
|
+
<div class="mt-2">${escapeHtml(error.message ?? 'Request failed.')}</div>
|
|
43
|
+
</div>
|
|
44
|
+
`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function renderSeverityBadge(severity = 'info') {
|
|
48
|
+
const normalized = String(severity ?? 'info').toLowerCase();
|
|
49
|
+
const className =
|
|
50
|
+
normalized === 'critical'
|
|
51
|
+
? 'border-error/35 bg-error-container/25 text-error'
|
|
52
|
+
: normalized === 'warning'
|
|
53
|
+
? 'border-primary-container/35 bg-primary-container/15 text-primary-container'
|
|
54
|
+
: 'border-outline-variant/20 bg-surface-container-lowest text-on-surface-variant';
|
|
55
|
+
|
|
56
|
+
return `<span class="inline-flex h-6 items-center border px-2 font-mono text-[10px] font-bold uppercase tracking-[0.16em] ${className}">${escapeHtml(
|
|
57
|
+
normalized,
|
|
58
|
+
)}</span>`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function renderRiskBadge(risk = 'low') {
|
|
62
|
+
const normalized = String(risk ?? 'low').toLowerCase();
|
|
63
|
+
const className =
|
|
64
|
+
normalized === 'high'
|
|
65
|
+
? 'border-error/35 text-error'
|
|
66
|
+
: normalized === 'medium'
|
|
67
|
+
? 'border-primary-container/35 text-primary-container'
|
|
68
|
+
: 'border-outline-variant/20 text-on-surface-variant/70';
|
|
69
|
+
|
|
70
|
+
return `<span class="inline-flex h-6 items-center border bg-surface-container-lowest px-2 font-mono text-[10px] font-bold uppercase tracking-[0.16em] ${className}">Risk ${escapeHtml(
|
|
71
|
+
normalized,
|
|
72
|
+
)}</span>`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function renderTableList(state) {
|
|
76
|
+
const { tables = [], selectedTableName } = state.tableAdvisor;
|
|
77
|
+
|
|
78
|
+
if (state.tableAdvisor.loading && !tables.length) {
|
|
79
|
+
return `
|
|
80
|
+
<div class="px-4 py-6 text-sm text-on-surface-variant/45">
|
|
81
|
+
Loading tables...
|
|
82
|
+
</div>
|
|
83
|
+
`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!tables.length) {
|
|
87
|
+
return `
|
|
88
|
+
<div class="px-4 py-6 text-sm text-on-surface-variant/45">
|
|
89
|
+
No tables found.
|
|
90
|
+
</div>
|
|
91
|
+
`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return `
|
|
95
|
+
<div class="subnavi-list custom-scrollbar">
|
|
96
|
+
${tables
|
|
97
|
+
.map(table => {
|
|
98
|
+
const isActive = table.name === selectedTableName;
|
|
99
|
+
|
|
100
|
+
return `
|
|
101
|
+
<a
|
|
102
|
+
class="table-designer-sidebar__item subnavi-item block border px-4 py-3 text-left transition-colors ${
|
|
103
|
+
isActive
|
|
104
|
+
? 'is-active border-primary-container/30 bg-surface-container-high'
|
|
105
|
+
: 'border-outline-variant/10 bg-surface-container-lowest hover:bg-surface-container-high'
|
|
106
|
+
}"
|
|
107
|
+
href="#/table-advisor/${encodeURIComponent(table.name)}"
|
|
108
|
+
title="${escapeHtml(table.name)}"
|
|
109
|
+
>
|
|
110
|
+
<div class="table-designer-sidebar__item-name ${isActive ? 'is-active' : ''}">
|
|
111
|
+
${escapeHtml(table.name)}
|
|
112
|
+
</div>
|
|
113
|
+
<div class="table-designer-sidebar__item-meta">
|
|
114
|
+
${escapeHtml(formatNumber(table.columnCount ?? 0))} columns
|
|
115
|
+
</div>
|
|
116
|
+
</a>
|
|
117
|
+
`;
|
|
118
|
+
})
|
|
119
|
+
.join('')}
|
|
120
|
+
</div>
|
|
121
|
+
`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function renderSummary(result = {}) {
|
|
125
|
+
const score = Number(result.score ?? 0);
|
|
126
|
+
|
|
127
|
+
return `
|
|
128
|
+
<div class="grid grid-cols-1 gap-3 md:grid-cols-4">
|
|
129
|
+
<div class="border border-outline-variant/10 bg-surface-container-low px-4 py-3">
|
|
130
|
+
<div class="font-mono text-[10px] uppercase tracking-[0.2em] text-on-surface-variant/55">Table</div>
|
|
131
|
+
<div class="mt-2 min-w-0 truncate text-lg font-black text-on-surface" title="${escapeHtml(
|
|
132
|
+
result.tableName ?? '',
|
|
133
|
+
)}">${escapeHtml(result.tableName ?? 'Unknown')}</div>
|
|
134
|
+
</div>
|
|
135
|
+
<div class="border border-outline-variant/10 bg-surface-container-low px-4 py-3">
|
|
136
|
+
<div class="font-mono text-[10px] uppercase tracking-[0.2em] text-on-surface-variant/55">Score</div>
|
|
137
|
+
<div class="mt-2 text-3xl font-black ${getScoreClass(score)}">${escapeHtml(formatNumber(score))}</div>
|
|
138
|
+
</div>
|
|
139
|
+
<div class="border border-outline-variant/10 bg-surface-container-low px-4 py-3">
|
|
140
|
+
<div class="font-mono text-[10px] uppercase tracking-[0.2em] text-on-surface-variant/55">Issues</div>
|
|
141
|
+
<div class="mt-2 text-3xl font-black text-on-surface">${escapeHtml(formatNumber(result.issueCount ?? 0))}</div>
|
|
142
|
+
</div>
|
|
143
|
+
<div class="border border-outline-variant/10 bg-surface-container-low px-4 py-3">
|
|
144
|
+
<div class="font-mono text-[10px] uppercase tracking-[0.2em] text-on-surface-variant/55">Rows</div>
|
|
145
|
+
<div class="mt-2 text-3xl font-black text-on-surface">${escapeHtml(formatNumber(result.rowCount ?? 0))}</div>
|
|
146
|
+
<div class="mt-1 truncate font-mono text-[10px] uppercase tracking-[0.14em] text-on-surface-variant/45">
|
|
147
|
+
${escapeHtml(formatDateTime(result.analyzedAt))}
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function renderIssue(issue = {}) {
|
|
155
|
+
const sql = String(issue.sql ?? '').trim();
|
|
156
|
+
|
|
157
|
+
return `
|
|
158
|
+
<article class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-4">
|
|
159
|
+
<div class="flex flex-wrap items-start justify-between gap-3">
|
|
160
|
+
<div class="min-w-0">
|
|
161
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
162
|
+
${renderSeverityBadge(issue.severity)}
|
|
163
|
+
${renderRiskBadge(issue.risk)}
|
|
164
|
+
</div>
|
|
165
|
+
<h4 class="mt-3 text-lg font-black text-on-surface">${escapeHtml(issue.title ?? 'Advisor issue')}</h4>
|
|
166
|
+
</div>
|
|
167
|
+
<div class="font-mono text-[10px] uppercase tracking-[0.16em] text-on-surface-variant/45" title="${escapeHtml(
|
|
168
|
+
issue.id ?? '',
|
|
169
|
+
)}">
|
|
170
|
+
${escapeHtml(truncateMiddle(issue.id ?? '', 42))}
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
<div class="mt-3 grid grid-cols-1 gap-3 text-sm leading-6 text-on-surface-variant/75 lg:grid-cols-3">
|
|
174
|
+
<div>
|
|
175
|
+
<div class="font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant/45">Explanation</div>
|
|
176
|
+
<p class="mt-1">${escapeHtml(issue.explanation ?? '')}</p>
|
|
177
|
+
</div>
|
|
178
|
+
<div>
|
|
179
|
+
<div class="font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant/45">Evidence</div>
|
|
180
|
+
<p class="mt-1">${escapeHtml(issue.evidence ?? '')}</p>
|
|
181
|
+
</div>
|
|
182
|
+
<div>
|
|
183
|
+
<div class="font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant/45">Recommendation</div>
|
|
184
|
+
<p class="mt-1">${escapeHtml(issue.recommendation ?? '')}</p>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
${
|
|
188
|
+
issue.risk === 'high'
|
|
189
|
+
? `<div class="mt-3 border border-error/20 bg-error-container/15 px-3 py-2 text-xs leading-5 text-error">
|
|
190
|
+
This change may require rebuilding the table. Create a backup before applying it.
|
|
191
|
+
</div>`
|
|
192
|
+
: ''
|
|
193
|
+
}
|
|
194
|
+
${
|
|
195
|
+
sql
|
|
196
|
+
? `<div class="mt-4 border border-outline-variant/10 bg-surface-container-low">
|
|
197
|
+
<div class="flex items-center justify-between gap-3 border-b border-outline-variant/10 px-3 py-2">
|
|
198
|
+
<div class="font-mono text-[10px] uppercase tracking-[0.18em] text-primary-container">SQL</div>
|
|
199
|
+
<button
|
|
200
|
+
class="standard-button min-h-8 px-3 py-1 text-[10px]"
|
|
201
|
+
data-action="copy-table-advisor-sql"
|
|
202
|
+
data-issue-id="${escapeHtml(issue.id ?? '')}"
|
|
203
|
+
type="button"
|
|
204
|
+
>
|
|
205
|
+
<span class="material-symbols-outlined text-sm">content_copy</span>
|
|
206
|
+
Copy SQL
|
|
207
|
+
</button>
|
|
208
|
+
</div>
|
|
209
|
+
<pre class="custom-scrollbar max-h-48 overflow-auto whitespace-pre-wrap px-3 py-3 text-xs leading-6 text-on-surface"><code>${highlightSql(
|
|
210
|
+
sql,
|
|
211
|
+
)}</code></pre>
|
|
212
|
+
</div>`
|
|
213
|
+
: ''
|
|
214
|
+
}
|
|
215
|
+
</article>
|
|
216
|
+
`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function renderIssues(result = {}) {
|
|
220
|
+
const issues = result.issues ?? [];
|
|
221
|
+
|
|
222
|
+
if (!issues.length) {
|
|
223
|
+
return `
|
|
224
|
+
<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-6 text-sm leading-6 text-on-surface-variant/70">
|
|
225
|
+
No table advisor issues found for this table.
|
|
226
|
+
</div>
|
|
227
|
+
`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return CATEGORY_ORDER.map(category => {
|
|
231
|
+
const categoryIssues = issues.filter(issue => issue.category === category);
|
|
232
|
+
|
|
233
|
+
if (!categoryIssues.length) {
|
|
234
|
+
return '';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return `
|
|
238
|
+
<section class="space-y-3">
|
|
239
|
+
<div class="flex items-center justify-between gap-3 border-b border-outline-variant/10 pb-2">
|
|
240
|
+
<h3 class="font-mono text-[11px] uppercase tracking-[0.22em] text-primary-container">
|
|
241
|
+
${escapeHtml(CATEGORY_LABELS[category] ?? category)}
|
|
242
|
+
</h3>
|
|
243
|
+
<div class="font-mono text-[10px] uppercase tracking-[0.16em] text-on-surface-variant/45">
|
|
244
|
+
${escapeHtml(formatNumber(categoryIssues.length))}
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
${categoryIssues.map(issue => renderIssue(issue)).join('')}
|
|
248
|
+
</section>
|
|
249
|
+
`;
|
|
250
|
+
}).join('');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function renderAdvisorBody(state) {
|
|
254
|
+
const advisor = state.tableAdvisor;
|
|
255
|
+
|
|
256
|
+
if (advisor.error) {
|
|
257
|
+
return renderError(advisor.error);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (advisor.loading && !advisor.result) {
|
|
261
|
+
return `
|
|
262
|
+
<div class="flex min-h-[280px] items-center justify-center border border-outline-variant/10 bg-surface-container-low">
|
|
263
|
+
<div class="text-center text-on-surface-variant/40">
|
|
264
|
+
<span class="material-symbols-outlined mb-3 text-4xl">progress_activity</span>
|
|
265
|
+
<p class="font-mono text-[10px] uppercase tracking-[0.22em]">LOADING_TABLES</p>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (!advisor.selectedTableName) {
|
|
272
|
+
return `
|
|
273
|
+
<div class="border border-dashed border-outline-variant/20 bg-surface-container-low px-8 py-10 text-center">
|
|
274
|
+
<span class="material-symbols-outlined mb-3 text-5xl text-on-surface-variant/25">troubleshoot</span>
|
|
275
|
+
<p class="font-body text-xl font-black uppercase tracking-tight text-primary-container">
|
|
276
|
+
No table selected
|
|
277
|
+
</p>
|
|
278
|
+
</div>
|
|
279
|
+
`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (advisor.analysisLoading) {
|
|
283
|
+
return `
|
|
284
|
+
<div class="flex min-h-[280px] items-center justify-center border border-outline-variant/10 bg-surface-container-low">
|
|
285
|
+
<div class="text-center text-on-surface-variant/40">
|
|
286
|
+
<span class="material-symbols-outlined mb-3 text-4xl">progress_activity</span>
|
|
287
|
+
<p class="font-mono text-[10px] uppercase tracking-[0.22em]">ANALYZING_TABLE</p>
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (advisor.analysisError) {
|
|
294
|
+
return renderError(advisor.analysisError);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (!advisor.result) {
|
|
298
|
+
return '';
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return `
|
|
302
|
+
<div class="space-y-6">
|
|
303
|
+
${renderSummary(advisor.result)}
|
|
304
|
+
${renderIssues(advisor.result)}
|
|
305
|
+
</div>
|
|
306
|
+
`;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function renderToolbar(state) {
|
|
310
|
+
const tableName = state.tableAdvisor.selectedTableName;
|
|
311
|
+
|
|
312
|
+
return `
|
|
313
|
+
<header class="workspace-header">
|
|
314
|
+
<div class="min-w-0">
|
|
315
|
+
<div class="font-mono text-[10px] uppercase tracking-[0.26em] text-primary-container/70">
|
|
316
|
+
Local analysis
|
|
317
|
+
</div>
|
|
318
|
+
<h1 class="mt-1 truncate font-body text-3xl font-black uppercase tracking-tight text-primary-container">
|
|
319
|
+
Table Advisor
|
|
320
|
+
</h1>
|
|
321
|
+
</div>
|
|
322
|
+
<div class="ml-auto flex flex-wrap items-center justify-end gap-3">
|
|
323
|
+
${renderWorkspaceOpenDropdown({
|
|
324
|
+
tableName: tableName ?? '',
|
|
325
|
+
destinations: [
|
|
326
|
+
{
|
|
327
|
+
icon: 'table_rows',
|
|
328
|
+
key: 'data',
|
|
329
|
+
label: 'Data',
|
|
330
|
+
target: name => `/data/${encodeURIComponent(name)}`,
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
icon: 'account_tree',
|
|
334
|
+
key: 'structure',
|
|
335
|
+
label: 'Structure',
|
|
336
|
+
target: name => `/structure/${encodeURIComponent(name)}`,
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
icon: 'table_chart',
|
|
340
|
+
key: 'table-designer',
|
|
341
|
+
label: 'Table Designer',
|
|
342
|
+
target: name => `/table-designer/${encodeURIComponent(name)}`,
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
key: 'sql-editor',
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
})}
|
|
349
|
+
<button class="standard-button" data-action="refresh-view" type="button">
|
|
350
|
+
<span class="material-symbols-outlined text-sm">refresh</span>
|
|
351
|
+
Refresh
|
|
352
|
+
</button>
|
|
353
|
+
</div>
|
|
354
|
+
</header>
|
|
355
|
+
`;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function renderTableAdvisorView(state) {
|
|
359
|
+
return {
|
|
360
|
+
main: `
|
|
361
|
+
<section class="view-surface flex h-full min-h-full min-h-0 flex-col overflow-hidden bg-surface-container">
|
|
362
|
+
<div class="data-view-grid data-view-grid--with-subnavi">
|
|
363
|
+
<aside class="subnavi-panel border-r border-outline-variant/10 bg-surface-low">
|
|
364
|
+
<div class="subnavi-header">
|
|
365
|
+
<div>
|
|
366
|
+
<div class="subnavi-header-title">Tables</div>
|
|
367
|
+
<div class="subnavi-header-details">
|
|
368
|
+
total ${escapeHtml(formatNumber(state.tableAdvisor.tables.length))}
|
|
369
|
+
</div>
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
${renderTableList(state)}
|
|
373
|
+
</aside>
|
|
374
|
+
<section class="flex min-h-0 flex-col overflow-hidden">
|
|
375
|
+
${renderToolbar(state)}
|
|
376
|
+
<div class="custom-scrollbar min-h-0 flex-1 overflow-auto p-6">
|
|
377
|
+
${renderAdvisorBody(state)}
|
|
378
|
+
</div>
|
|
379
|
+
</section>
|
|
380
|
+
</div>
|
|
381
|
+
</section>
|
|
382
|
+
`,
|
|
383
|
+
panel: '',
|
|
384
|
+
};
|
|
385
|
+
}
|
|
@@ -51,6 +51,12 @@ function renderWorkspaceToolbar(state) {
|
|
|
51
51
|
label: "Structure",
|
|
52
52
|
target: name => `/structure/${encodeURIComponent(name)}`,
|
|
53
53
|
},
|
|
54
|
+
{
|
|
55
|
+
icon: "troubleshoot",
|
|
56
|
+
key: "table-advisor",
|
|
57
|
+
label: "Table Advisor",
|
|
58
|
+
target: name => `/table-advisor/${encodeURIComponent(name)}`,
|
|
59
|
+
},
|
|
54
60
|
{
|
|
55
61
|
key: "sql-editor",
|
|
56
62
|
},
|
package/frontend/styles/base.css
CHANGED
|
@@ -129,11 +129,6 @@ img {
|
|
|
129
129
|
display: none !important;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
.font-headline {
|
|
133
|
-
font-family: var(--font-family-headline);
|
|
134
|
-
word-break: break-all;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
132
|
.font-body {
|
|
138
133
|
font-family: var(--font-family-body);
|
|
139
134
|
}
|
|
@@ -159,17 +154,6 @@ img {
|
|
|
159
154
|
vertical-align: middle;
|
|
160
155
|
}
|
|
161
156
|
|
|
162
|
-
.no-scrollbar {
|
|
163
|
-
-ms-overflow-style: none;
|
|
164
|
-
scrollbar-width: none;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
.no-scrollbar::-webkit-scrollbar {
|
|
168
|
-
display: none;
|
|
169
|
-
height: 0;
|
|
170
|
-
width: 0;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
157
|
.custom-scrollbar {
|
|
174
158
|
scrollbar-color: var(--color-surface-highest) var(--color-surface-lowest);
|
|
175
159
|
scrollbar-width: thin;
|
|
@@ -193,10 +177,7 @@ img {
|
|
|
193
177
|
}
|
|
194
178
|
|
|
195
179
|
.clipped-btn,
|
|
196
|
-
.clipped-corner
|
|
197
|
-
.custom-clip,
|
|
198
|
-
.clip-button,
|
|
199
|
-
.clip-corner {
|
|
180
|
+
.clipped-corner {
|
|
200
181
|
clip-path: var(--clip-path);
|
|
201
182
|
}
|
|
202
183
|
|
|
@@ -274,28 +255,12 @@ img {
|
|
|
274
255
|
color: transparent;
|
|
275
256
|
}
|
|
276
257
|
|
|
277
|
-
.cursor-blink {
|
|
278
|
-
animation: cursor-blink var(--duration-cursor-blink) step-end infinite;
|
|
279
|
-
border-right: 2px solid var(--color-primary-container);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
258
|
.text-xs {
|
|
283
259
|
white-space: nowrap;
|
|
284
260
|
overflow: hidden;
|
|
285
261
|
text-overflow: ellipsis;
|
|
286
262
|
}
|
|
287
263
|
|
|
288
|
-
@keyframes cursor-blink {
|
|
289
|
-
from,
|
|
290
|
-
to {
|
|
291
|
-
border-color: transparent;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
50% {
|
|
295
|
-
border-color: var(--color-primary-container);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
264
|
@media (prefers-reduced-motion: reduce) {
|
|
300
265
|
*,
|
|
301
266
|
*::before,
|
|
@@ -306,8 +271,4 @@ img {
|
|
|
306
271
|
transition-duration: 0.001ms;
|
|
307
272
|
}
|
|
308
273
|
|
|
309
|
-
.cursor-blink {
|
|
310
|
-
animation: none;
|
|
311
|
-
border-right-color: var(--color-primary-container);
|
|
312
|
-
}
|
|
313
274
|
}
|