sqlite-hub 1.1.2 → 1.4.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 +106 -73
- package/bin/sqlite-hub.js +176 -2
- package/docs/API.md +122 -0
- package/docs/CLI.md +227 -0
- package/docs/DESIGN_GUIDELINES.md +45 -0
- package/docs/changelog.md +141 -0
- package/docs/shortkeys.md +27 -0
- package/docs/todo.md +16 -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 +18 -0
- package/frontend/js/app.js +209 -13
- package/frontend/js/components/connectionCard.js +1 -1
- 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 +211 -18
- 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/queryResults.js +1 -1
- package/frontend/js/components/rowEditorPanel.js +65 -16
- package/frontend/js/components/topNav.js +1 -4
- package/frontend/js/components/workspaceOpenDropdown.js +52 -0
- package/frontend/js/router.js +28 -6
- package/frontend/js/store.js +489 -9
- package/frontend/js/utils/emailPreview.js +28 -0
- package/frontend/js/utils/markdownDocuments.js +17 -1
- package/frontend/js/views/backups.js +556 -36
- package/frontend/js/views/charts.js +9 -9
- package/frontend/js/views/connections.js +2 -2
- package/frontend/js/views/data.js +43 -17
- package/frontend/js/views/documents.js +3 -3
- package/frontend/js/views/editor.js +6 -6
- package/frontend/js/views/mediaTagging.js +7 -5
- package/frontend/js/views/overview.js +3 -3
- package/frontend/js/views/settings.js +4 -3
- package/frontend/js/views/structure.js +27 -10
- package/frontend/js/views/tableDesigner.js +23 -0
- package/frontend/styles/base.css +1 -40
- package/frontend/styles/components.css +114 -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 +29 -21
- package/frontend/styles/views.css +94 -261
- package/package.json +18 -2
- package/server/routes/backups.js +18 -2
- package/server/routes/externalApi.js +36 -0
- package/server/routes/structure.js +22 -0
- package/server/services/databaseCommandService.js +14 -0
- package/server/services/sqlite/backupDiff.js +914 -0
- package/server/services/sqlite/backupService.js +77 -1
- package/server/services/sqlite/structureService.js +7 -0
- 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 -64
- 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 -75
- 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 -79
- 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 -155
- 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 -107
- 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 -56
- 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,14 @@
|
|
|
1
1
|
import { renderStatusBadge } from '../components/badges.js';
|
|
2
2
|
import { renderPageHeader } from '../components/pageHeader.js';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
escapeHtml,
|
|
5
|
+
formatBytes,
|
|
6
|
+
formatCellValue,
|
|
7
|
+
formatCompactDateTime,
|
|
8
|
+
formatDateTime,
|
|
9
|
+
formatNumber,
|
|
10
|
+
truncateMiddle,
|
|
11
|
+
} from '../utils/format.js';
|
|
4
12
|
|
|
5
13
|
const STATUS_TONE = {
|
|
6
14
|
creating: 'muted',
|
|
@@ -28,38 +36,92 @@ function isBackupBusy(backup, state) {
|
|
|
28
36
|
return Boolean(state.backups.operationLoading) || ['creating', 'verifying', 'restoring'].includes(backup.status);
|
|
29
37
|
}
|
|
30
38
|
|
|
39
|
+
function isBackupForActiveConnection(backup, state) {
|
|
40
|
+
const activeConnection = state.connections.active;
|
|
41
|
+
|
|
42
|
+
if (!activeConnection) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (backup.connectionId && String(backup.connectionId) === String(activeConnection.id)) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return Boolean(backup.sourcePath && activeConnection.path && backup.sourcePath === activeConnection.path);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function renderBackupMetadataItem(label, value, extraClass = '') {
|
|
54
|
+
return `
|
|
55
|
+
<div class="min-w-0 border border-outline-variant/10 bg-surface-container-lowest px-3 py-2 ${extraClass}">
|
|
56
|
+
<div class="font-mono text-[9px] uppercase tracking-[0.16em] text-on-surface-variant/45">${escapeHtml(label)}</div>
|
|
57
|
+
<div class="mt-1 break-words font-mono text-[10px] uppercase tracking-[0.1em] text-on-surface/80">${escapeHtml(value)}</div>
|
|
58
|
+
</div>
|
|
59
|
+
`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function renderBackupMetadata(backup) {
|
|
63
|
+
return `
|
|
64
|
+
<div class="grid min-w-0 grid-cols-2 gap-2">
|
|
65
|
+
${renderBackupMetadataItem('Size', formatBytes(backup.sizeBytes))}
|
|
66
|
+
<div class="min-w-0 border border-outline-variant/10 bg-surface-container-lowest px-3 py-2">
|
|
67
|
+
<div class="font-mono text-[9px] uppercase tracking-[0.16em] text-on-surface-variant/45">Status</div>
|
|
68
|
+
<div class="mt-1">${renderBackupStatus(backup)}</div>
|
|
69
|
+
</div>
|
|
70
|
+
${renderBackupMetadataItem('SQLite Hub', backup.sqliteHubVersion ? `v${backup.sqliteHubVersion}` : 'n/a')}
|
|
71
|
+
${renderBackupMetadataItem('SQLite', backup.sqliteVersion ? `v${backup.sqliteVersion}` : 'n/a')}
|
|
72
|
+
</div>
|
|
73
|
+
`;
|
|
74
|
+
}
|
|
75
|
+
|
|
31
76
|
function renderBackupRows(state) {
|
|
32
77
|
return state.backups.items
|
|
33
78
|
.map(backup => {
|
|
34
79
|
const busy = isBackupBusy(backup, state);
|
|
35
80
|
const canRestore = backup.status === 'verified' && backup.fileExists && !busy && !state.connections.active?.readOnly;
|
|
81
|
+
const canCompare =
|
|
82
|
+
backup.status === 'verified' && backup.fileExists && !busy && isBackupForActiveConnection(backup, state);
|
|
36
83
|
const canDownload = backup.fileExists && !busy;
|
|
37
|
-
const
|
|
84
|
+
const canEdit = !busy;
|
|
38
85
|
const canDelete = !busy;
|
|
39
86
|
|
|
40
87
|
return `
|
|
41
|
-
<
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
88
|
+
<div class="grid min-w-[76rem] gap-5 px-4 py-5 xl:grid-cols-[minmax(18rem,1.2fr)_minmax(17rem,0.85fr)_minmax(18rem,1fr)_14rem]">
|
|
89
|
+
<div class="min-w-0 space-y-2">
|
|
90
|
+
<div class="break-words font-body text-sm font-black uppercase text-on-surface">${escapeHtml(backup.name)}</div>
|
|
91
|
+
<div class="font-mono text-[10px] uppercase tracking-[0.12em] text-on-surface-variant/45" title="${escapeHtml(backup.path)}">
|
|
92
|
+
${escapeHtml(truncateMiddle(backup.fileName || backup.path, 48))}
|
|
93
|
+
</div>
|
|
94
|
+
<div class="font-mono text-[10px] uppercase tracking-[0.12em] text-on-surface-variant/55">
|
|
95
|
+
Created // ${escapeHtml(formatCompactDateTime(backup.createdAt))}
|
|
96
|
+
</div>
|
|
97
|
+
<div class="font-mono text-[10px] uppercase tracking-[0.12em] text-on-surface-variant/45">
|
|
98
|
+
PK // ${escapeHtml(backup.id)}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="min-w-0">
|
|
102
|
+
${renderBackupMetadata(backup)}
|
|
103
|
+
</div>
|
|
104
|
+
<div class="min-w-0">
|
|
105
|
+
<div class="mb-2 flex items-center justify-between gap-3">
|
|
106
|
+
<div class="font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant/45">Note</div>
|
|
107
|
+
<button class="standard-button flex-none" data-action="open-edit-backup-modal" data-backup-id="${escapeHtml(backup.id)}" type="button" ${canEdit ? '' : 'disabled'}>
|
|
56
108
|
<span class="material-symbols-outlined text-sm">edit_note</span>
|
|
57
|
-
Edit
|
|
109
|
+
Edit
|
|
58
110
|
</button>
|
|
59
111
|
</div>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
112
|
+
<div class="min-h-20 border border-outline-variant/10 bg-surface-container-lowest px-3 py-3">
|
|
113
|
+
<div class="max-w-full whitespace-pre-wrap break-words text-xs leading-6 text-on-surface-variant/70 [overflow-wrap:anywhere]">
|
|
114
|
+
${backup.notes ? escapeHtml(backup.notes) : '<span class="text-on-surface-variant/35">No notes</span>'}
|
|
115
|
+
${backup.errorMessage ? `<div class="mt-2 text-error">${escapeHtml(backup.errorMessage)}</div>` : ''}
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
<div class="min-w-0">
|
|
120
|
+
<div class="flex min-w-0 flex-col items-stretch gap-2">
|
|
121
|
+
<button class="standard-button" data-action="open-compare-backup-drawer" data-backup-id="${escapeHtml(backup.id)}" type="button" ${canCompare ? '' : 'disabled'}>
|
|
122
|
+
<span class="material-symbols-outlined text-sm">difference</span>
|
|
123
|
+
Compare with current
|
|
124
|
+
</button>
|
|
63
125
|
<button class="standard-button" data-action="open-restore-backup-modal" data-backup-id="${escapeHtml(backup.id)}" type="button" ${canRestore ? '' : 'disabled'}>
|
|
64
126
|
<span class="material-symbols-outlined text-sm">restore</span>
|
|
65
127
|
Restore
|
|
@@ -73,8 +135,8 @@ function renderBackupRows(state) {
|
|
|
73
135
|
Delete
|
|
74
136
|
</button>
|
|
75
137
|
</div>
|
|
76
|
-
</
|
|
77
|
-
</
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
78
140
|
`;
|
|
79
141
|
})
|
|
80
142
|
.join('');
|
|
@@ -95,7 +157,7 @@ function renderBackupsBody(state) {
|
|
|
95
157
|
if (state.backups.error) {
|
|
96
158
|
return `
|
|
97
159
|
<div class="border border-error/20 bg-error-container/10 px-6 py-5 text-sm text-on-surface">
|
|
98
|
-
<div class="font-
|
|
160
|
+
<div class="font-body text-xs font-bold uppercase tracking-[0.18em] text-error">
|
|
99
161
|
${escapeHtml(state.backups.error.code)}
|
|
100
162
|
</div>
|
|
101
163
|
<div class="mt-2">${escapeHtml(state.backups.error.message)}</div>
|
|
@@ -107,7 +169,7 @@ function renderBackupsBody(state) {
|
|
|
107
169
|
return `
|
|
108
170
|
<div class="border border-dashed border-outline-variant/20 bg-surface-container-low px-8 py-10 text-center">
|
|
109
171
|
<span class="material-symbols-outlined mb-3 text-5xl text-on-surface-variant/25">inventory_2</span>
|
|
110
|
-
<p class="font-
|
|
172
|
+
<p class="font-body text-xl font-black uppercase tracking-tight text-primary-container">
|
|
111
173
|
No backups yet
|
|
112
174
|
</p>
|
|
113
175
|
<p class="mx-auto mt-3 max-w-xl text-sm leading-7 text-on-surface-variant/65">
|
|
@@ -125,25 +187,483 @@ function renderBackupsBody(state) {
|
|
|
125
187
|
|
|
126
188
|
return `
|
|
127
189
|
<div class="overflow-auto border border-outline-variant/10 bg-surface-container-low">
|
|
128
|
-
<
|
|
129
|
-
<
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
190
|
+
<div class="grid min-w-[76rem] grid-cols-[minmax(18rem,1.2fr)_minmax(17rem,0.85fr)_minmax(18rem,1fr)_14rem] gap-5 border-b border-outline-variant/10 bg-surface-container px-4 py-3 font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
191
|
+
<div>Backup</div>
|
|
192
|
+
<div>Metadata</div>
|
|
193
|
+
<div>Note</div>
|
|
194
|
+
<div class="text-right">Actions</div>
|
|
195
|
+
</div>
|
|
196
|
+
<div class="divide-y divide-outline-variant/10">
|
|
197
|
+
${renderBackupRows(state)}
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function formatDiffMetric(value) {
|
|
204
|
+
return value === null || value === undefined ? '-' : formatNumber(value);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function formatDiffValue(value) {
|
|
208
|
+
if (value && typeof value === 'object' && value.__type === 'integer') {
|
|
209
|
+
return value.value;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return formatCellValue(value);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function hasBackupDiffChanges(summary = {}) {
|
|
216
|
+
return Boolean(
|
|
217
|
+
Number(summary.schemaChanges ?? 0) ||
|
|
218
|
+
Number(summary.rowsAdded ?? 0) ||
|
|
219
|
+
Number(summary.rowsChanged ?? 0) ||
|
|
220
|
+
Number(summary.rowsRemoved ?? 0) ||
|
|
221
|
+
Number(summary.skippedTables ?? 0),
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function renderBackupDiffMetaItem(label, value) {
|
|
226
|
+
return `
|
|
227
|
+
<div class="border border-outline-variant/10 bg-surface-container px-3 py-3">
|
|
228
|
+
<div class="font-mono text-[10px] uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
229
|
+
${escapeHtml(label)}
|
|
230
|
+
</div>
|
|
231
|
+
<div class="mt-2 break-words text-sm text-on-surface">${escapeHtml(value || '-')}</div>
|
|
232
|
+
</div>
|
|
233
|
+
`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function renderBackupDiffSummary(summary = {}) {
|
|
237
|
+
const items = [
|
|
238
|
+
['Schema changes', summary.schemaChanges],
|
|
239
|
+
['Rows added', summary.rowsAdded],
|
|
240
|
+
['Rows changed', summary.rowsChanged],
|
|
241
|
+
['Rows removed', summary.rowsRemoved],
|
|
242
|
+
['Skipped tables', summary.skippedTables],
|
|
243
|
+
];
|
|
244
|
+
|
|
245
|
+
return `
|
|
246
|
+
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
247
|
+
${items.map(([label, value]) => renderBackupDiffMetaItem(label, formatDiffMetric(value))).join('')}
|
|
248
|
+
</div>
|
|
249
|
+
`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function renderBackupDiffTabs(activeTab) {
|
|
253
|
+
return `
|
|
254
|
+
<div class="mt-5 flex flex-col items-start gap-2">
|
|
255
|
+
<div class="charts-height-toggle" role="group" aria-label="Backup diff view">
|
|
256
|
+
<button
|
|
257
|
+
class="standard-button charts-height-toggle__button ${activeTab === 'schema' ? 'is-active' : ''}"
|
|
258
|
+
aria-pressed="${activeTab === 'schema' ? 'true' : 'false'}"
|
|
259
|
+
data-action="set-backup-diff-tab"
|
|
260
|
+
data-tab="schema"
|
|
261
|
+
type="button"
|
|
262
|
+
>
|
|
263
|
+
Schema
|
|
264
|
+
</button>
|
|
265
|
+
<button
|
|
266
|
+
class="standard-button charts-height-toggle__button ${activeTab === 'data' ? 'is-active' : ''}"
|
|
267
|
+
aria-pressed="${activeTab === 'data' ? 'true' : 'false'}"
|
|
268
|
+
data-action="set-backup-diff-tab"
|
|
269
|
+
data-tab="data"
|
|
270
|
+
type="button"
|
|
271
|
+
>
|
|
272
|
+
Data
|
|
273
|
+
</button>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function getSchemaObjectLabel(objectType) {
|
|
280
|
+
return (
|
|
281
|
+
{
|
|
282
|
+
table: 'table',
|
|
283
|
+
column: 'column',
|
|
284
|
+
index: 'index',
|
|
285
|
+
foreign_key: 'foreign key',
|
|
286
|
+
view: 'view',
|
|
287
|
+
trigger: 'trigger',
|
|
288
|
+
}[objectType] || objectType || 'object'
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function getSchemaActionIcon(action) {
|
|
293
|
+
return (
|
|
294
|
+
{
|
|
295
|
+
added: '+',
|
|
296
|
+
changed: '~',
|
|
297
|
+
removed: '-',
|
|
298
|
+
}[action] || '*'
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function getSchemaActionLabel(action) {
|
|
303
|
+
return (
|
|
304
|
+
{
|
|
305
|
+
added: 'Added',
|
|
306
|
+
changed: 'Changed',
|
|
307
|
+
removed: 'Removed',
|
|
308
|
+
}[action] || 'Updated'
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function renderSchemaDefinitionPair(change) {
|
|
313
|
+
if (!change.before && !change.after) {
|
|
314
|
+
return '';
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return `
|
|
318
|
+
<div class="mt-2 grid gap-2 text-[11px]">
|
|
319
|
+
<div class="border border-outline-variant/10 bg-surface-container px-3 py-2">
|
|
320
|
+
<div class="font-mono text-[9px] uppercase tracking-[0.16em] text-on-surface-variant/45">Backup</div>
|
|
321
|
+
<div class="mt-1 whitespace-pre-wrap break-words font-mono text-on-surface-variant/75 [overflow-wrap:anywhere]">${escapeHtml(change.before || '-')}</div>
|
|
322
|
+
</div>
|
|
323
|
+
<div class="border border-outline-variant/10 bg-surface-container px-3 py-2">
|
|
324
|
+
<div class="font-mono text-[9px] uppercase tracking-[0.16em] text-on-surface-variant/45">Current</div>
|
|
325
|
+
<div class="mt-1 whitespace-pre-wrap break-words font-mono text-on-surface-variant/75 [overflow-wrap:anywhere]">${escapeHtml(change.after || '-')}</div>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
`;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function renderSchemaChange(change) {
|
|
332
|
+
const name = change.name || change.definition || getSchemaObjectLabel(change.objectType);
|
|
333
|
+
const detail =
|
|
334
|
+
change.action === 'changed'
|
|
335
|
+
? ''
|
|
336
|
+
: change.definition && change.definition !== name
|
|
337
|
+
? `: ${change.definition}`
|
|
338
|
+
: '';
|
|
339
|
+
|
|
340
|
+
return `
|
|
341
|
+
<div class="border-l border-outline-variant/20 pl-3">
|
|
342
|
+
<div class="font-mono text-[11px] uppercase tracking-[0.12em] text-on-surface/80">
|
|
343
|
+
${escapeHtml(getSchemaActionIcon(change.action))}
|
|
344
|
+
${escapeHtml(getSchemaActionLabel(change.action))}
|
|
345
|
+
${escapeHtml(getSchemaObjectLabel(change.objectType))}: ${escapeHtml(name)}${escapeHtml(detail)}
|
|
346
|
+
</div>
|
|
347
|
+
${renderSchemaDefinitionPair(change)}
|
|
348
|
+
</div>
|
|
349
|
+
`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function renderSchemaEntry(entry, fallbackAction) {
|
|
353
|
+
const changes = Array.isArray(entry.changes)
|
|
354
|
+
? entry.changes
|
|
355
|
+
: [
|
|
356
|
+
{
|
|
357
|
+
action: fallbackAction,
|
|
358
|
+
objectType: entry.type,
|
|
359
|
+
name: entry.name,
|
|
360
|
+
definition: entry.definition,
|
|
361
|
+
},
|
|
362
|
+
];
|
|
363
|
+
|
|
364
|
+
return `
|
|
365
|
+
<div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-4">
|
|
366
|
+
<div class="break-words font-body text-sm font-black uppercase text-on-surface">${escapeHtml(entry.name)}</div>
|
|
367
|
+
<div class="mt-3 space-y-3">
|
|
368
|
+
${changes.map(renderSchemaChange).join('')}
|
|
369
|
+
</div>
|
|
370
|
+
</div>
|
|
371
|
+
`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function renderSchemaGroup(title, entries, fallbackAction) {
|
|
375
|
+
return `
|
|
376
|
+
<section class="space-y-3">
|
|
377
|
+
<div class="font-mono text-[10px] uppercase tracking-[0.22em] text-on-surface-variant/55">${escapeHtml(title)}</div>
|
|
378
|
+
${
|
|
379
|
+
entries?.length
|
|
380
|
+
? `<div class="space-y-3">${entries.map(entry => renderSchemaEntry(entry, fallbackAction)).join('')}</div>`
|
|
381
|
+
: '<div class="border border-dashed border-outline-variant/15 bg-surface-container px-4 py-3 text-sm text-on-surface-variant/50">No changes in this group.</div>'
|
|
382
|
+
}
|
|
383
|
+
</section>
|
|
384
|
+
`;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function renderBackupDiffSchema(diff) {
|
|
388
|
+
const schema = diff?.schema ?? { added: [], changed: [], removed: [] };
|
|
389
|
+
|
|
390
|
+
return `
|
|
391
|
+
<div class="mt-5 space-y-5">
|
|
392
|
+
${renderSchemaGroup('Added', schema.added, 'added')}
|
|
393
|
+
${renderSchemaGroup('Changed', schema.changed, 'changed')}
|
|
394
|
+
${renderSchemaGroup('Removed', schema.removed, 'removed')}
|
|
395
|
+
</div>
|
|
396
|
+
`;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function renderSampleValueRows(sample, valueLabel) {
|
|
400
|
+
const entries = Object.entries(sample.values ?? {});
|
|
401
|
+
|
|
402
|
+
if (!entries.length) {
|
|
403
|
+
return '';
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return `
|
|
407
|
+
<div class="overflow-auto border border-outline-variant/10">
|
|
408
|
+
<table class="min-w-full border-collapse text-left text-xs">
|
|
409
|
+
<thead class="bg-surface-container">
|
|
410
|
+
<tr class="font-mono text-[10px] uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
411
|
+
<th class="px-3 py-2 font-normal">Column</th>
|
|
412
|
+
<th class="px-3 py-2 font-normal">${escapeHtml(valueLabel)}</th>
|
|
137
413
|
</tr>
|
|
138
414
|
</thead>
|
|
139
415
|
<tbody>
|
|
140
|
-
${
|
|
416
|
+
${entries
|
|
417
|
+
.map(
|
|
418
|
+
([column, value]) => `
|
|
419
|
+
<tr class="border-t border-outline-variant/10">
|
|
420
|
+
<td class="px-3 py-2 font-mono text-on-surface-variant/70">${escapeHtml(column)}</td>
|
|
421
|
+
<td class="px-3 py-2 text-on-surface">${escapeHtml(formatDiffValue(value))}</td>
|
|
422
|
+
</tr>
|
|
423
|
+
`,
|
|
424
|
+
)
|
|
425
|
+
.join('')}
|
|
141
426
|
</tbody>
|
|
142
427
|
</table>
|
|
143
428
|
</div>
|
|
144
429
|
`;
|
|
145
430
|
}
|
|
146
431
|
|
|
432
|
+
function renderChangedRowSample(sample) {
|
|
433
|
+
return `
|
|
434
|
+
<div class="space-y-2 border border-outline-variant/10 bg-surface-container-lowest px-4 py-3">
|
|
435
|
+
<div class="break-words font-body text-sm font-black text-on-surface">${escapeHtml(sample.identityLabel || 'Row')}</div>
|
|
436
|
+
<div class="overflow-auto border border-outline-variant/10">
|
|
437
|
+
<table class="min-w-full border-collapse text-left text-xs">
|
|
438
|
+
<thead class="bg-surface-container">
|
|
439
|
+
<tr class="font-mono text-[10px] uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
440
|
+
<th class="px-3 py-2 font-normal">Column</th>
|
|
441
|
+
<th class="px-3 py-2 font-normal">Backup</th>
|
|
442
|
+
<th class="px-3 py-2 font-normal">Current</th>
|
|
443
|
+
</tr>
|
|
444
|
+
</thead>
|
|
445
|
+
<tbody>
|
|
446
|
+
${(sample.columns ?? [])
|
|
447
|
+
.map(
|
|
448
|
+
column => `
|
|
449
|
+
<tr class="border-t border-outline-variant/10">
|
|
450
|
+
<td class="px-3 py-2 font-mono text-on-surface-variant/70">${escapeHtml(column.name)}</td>
|
|
451
|
+
<td class="px-3 py-2 text-on-surface">${escapeHtml(formatDiffValue(column.backup))}</td>
|
|
452
|
+
<td class="px-3 py-2 text-on-surface">${escapeHtml(formatDiffValue(column.current))}</td>
|
|
453
|
+
</tr>
|
|
454
|
+
`,
|
|
455
|
+
)
|
|
456
|
+
.join('')}
|
|
457
|
+
</tbody>
|
|
458
|
+
</table>
|
|
459
|
+
</div>
|
|
460
|
+
</div>
|
|
461
|
+
`;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function renderAddedOrRemovedSample(sample, valueLabel) {
|
|
465
|
+
return `
|
|
466
|
+
<div class="space-y-2 border border-outline-variant/10 bg-surface-container-lowest px-4 py-3">
|
|
467
|
+
${sample.identityLabel ? `<div class="break-words font-body text-sm font-black text-on-surface">${escapeHtml(sample.identityLabel)}</div>` : ''}
|
|
468
|
+
${renderSampleValueRows(sample, valueLabel)}
|
|
469
|
+
</div>
|
|
470
|
+
`;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function renderDataSampleGroup(title, samples, renderer) {
|
|
474
|
+
if (!samples?.length) {
|
|
475
|
+
return '';
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return `
|
|
479
|
+
<section class="space-y-3">
|
|
480
|
+
<div class="font-mono text-[10px] uppercase tracking-[0.2em] text-on-surface-variant/50">${escapeHtml(title)}</div>
|
|
481
|
+
<div class="space-y-3">${samples.map(renderer).join('')}</div>
|
|
482
|
+
</section>
|
|
483
|
+
`;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function renderDataTableDetails(table) {
|
|
487
|
+
const sampleMarkup = [
|
|
488
|
+
renderDataSampleGroup('Rows added', table.samples?.added, sample =>
|
|
489
|
+
renderAddedOrRemovedSample(sample, 'Current'),
|
|
490
|
+
),
|
|
491
|
+
renderDataSampleGroup('Rows changed', table.samples?.changed, renderChangedRowSample),
|
|
492
|
+
renderDataSampleGroup('Rows removed', table.samples?.removed, sample =>
|
|
493
|
+
renderAddedOrRemovedSample(sample, 'Backup'),
|
|
494
|
+
),
|
|
495
|
+
]
|
|
496
|
+
.filter(Boolean)
|
|
497
|
+
.join('');
|
|
498
|
+
|
|
499
|
+
if (!sampleMarkup) {
|
|
500
|
+
return `
|
|
501
|
+
<div class="border-t border-outline-variant/10 px-4 py-3 text-sm text-on-surface-variant/50">
|
|
502
|
+
No sample rows returned for this table.
|
|
503
|
+
</div>
|
|
504
|
+
`;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return `<div class="space-y-4 border-t border-outline-variant/10 px-4 py-4">${sampleMarkup}</div>`;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function renderDataTableRow(table) {
|
|
511
|
+
const canExpand =
|
|
512
|
+
table.status === 'comparable' &&
|
|
513
|
+
((table.samples?.added?.length ?? 0) ||
|
|
514
|
+
(table.samples?.changed?.length ?? 0) ||
|
|
515
|
+
(table.samples?.removed?.length ?? 0));
|
|
516
|
+
const summary = `
|
|
517
|
+
<div class="grid grid-cols-[minmax(10rem,1fr)_4rem_4rem_4rem] items-center gap-3 px-4 py-3">
|
|
518
|
+
<div class="min-w-0">
|
|
519
|
+
<div class="break-words font-body text-sm font-black uppercase text-on-surface">${escapeHtml(table.name)}</div>
|
|
520
|
+
<div class="mt-1 text-xs text-on-surface-variant/60" title="${escapeHtml(table.reason ?? '')}">
|
|
521
|
+
${escapeHtml(table.statusLabel || table.status)}
|
|
522
|
+
</div>
|
|
523
|
+
</div>
|
|
524
|
+
<div class="font-mono text-xs text-on-surface">${escapeHtml(formatDiffMetric(table.added))}</div>
|
|
525
|
+
<div class="font-mono text-xs text-on-surface">${escapeHtml(formatDiffMetric(table.changed))}</div>
|
|
526
|
+
<div class="font-mono text-xs text-on-surface">${escapeHtml(formatDiffMetric(table.removed))}</div>
|
|
527
|
+
</div>
|
|
528
|
+
`;
|
|
529
|
+
|
|
530
|
+
if (!canExpand) {
|
|
531
|
+
return `<div class="border-t border-outline-variant/10">${summary}</div>`;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return `
|
|
535
|
+
<details class="border-t border-outline-variant/10">
|
|
536
|
+
<summary class="cursor-pointer list-none">${summary}</summary>
|
|
537
|
+
${renderDataTableDetails(table)}
|
|
538
|
+
</details>
|
|
539
|
+
`;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function renderBackupDiffData(diff) {
|
|
543
|
+
const tables = diff?.data?.tables ?? [];
|
|
544
|
+
|
|
545
|
+
if (!tables.length) {
|
|
546
|
+
return `
|
|
547
|
+
<div class="mt-5 border border-dashed border-outline-variant/15 bg-surface-container px-4 py-3 text-sm text-on-surface-variant/50">
|
|
548
|
+
No user tables were found in either database state.
|
|
549
|
+
</div>
|
|
550
|
+
`;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return `
|
|
554
|
+
<div class="mt-5 overflow-auto border border-outline-variant/10 bg-surface-container-lowest">
|
|
555
|
+
<div class="grid grid-cols-[minmax(10rem,1fr)_4rem_4rem_4rem] gap-3 bg-surface-container px-4 py-3 font-mono text-[10px] uppercase tracking-[0.16em] text-on-surface-variant/55">
|
|
556
|
+
<div>Table</div>
|
|
557
|
+
<div>Added</div>
|
|
558
|
+
<div>Changed</div>
|
|
559
|
+
<div>Removed</div>
|
|
560
|
+
</div>
|
|
561
|
+
${tables.map(renderDataTableRow).join('')}
|
|
562
|
+
</div>
|
|
563
|
+
`;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function renderBackupDiffDrawerBody(diffState) {
|
|
567
|
+
if (diffState.loading) {
|
|
568
|
+
return `
|
|
569
|
+
<div class="flex flex-1 items-center justify-center px-6 text-[10px] font-mono uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
570
|
+
Comparing backup...
|
|
571
|
+
</div>
|
|
572
|
+
`;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (diffState.error) {
|
|
576
|
+
return `
|
|
577
|
+
<div class="p-5">
|
|
578
|
+
<div class="border border-error/30 bg-error-container/20 px-4 py-4 text-sm text-error">
|
|
579
|
+
<div class="font-mono text-[10px] uppercase tracking-[0.18em]">${escapeHtml(diffState.error.code || 'REQUEST_FAILED')}</div>
|
|
580
|
+
<div class="mt-2">${escapeHtml(diffState.error.message || 'Backup comparison failed.')}</div>
|
|
581
|
+
</div>
|
|
582
|
+
</div>
|
|
583
|
+
`;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const diff = diffState.data;
|
|
587
|
+
|
|
588
|
+
if (!diff) {
|
|
589
|
+
return '';
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const activeTab = diffState.activeTab === 'data' ? 'data' : 'schema';
|
|
593
|
+
|
|
594
|
+
return `
|
|
595
|
+
<div class="custom-scrollbar min-h-0 flex-1 overflow-auto px-5 py-5">
|
|
596
|
+
<div class="grid grid-cols-1 gap-3">
|
|
597
|
+
${renderBackupDiffMetaItem('Created', formatDateTime(diff.backup?.createdAt ?? diffState.backupCreatedAt))}
|
|
598
|
+
${renderBackupDiffMetaItem('Current', diff.current?.label || diffState.currentLabel || 'Current database')}
|
|
599
|
+
</div>
|
|
600
|
+
<section class="mt-5">
|
|
601
|
+
${renderBackupDiffSummary(diff.summary)}
|
|
602
|
+
</section>
|
|
603
|
+
${
|
|
604
|
+
hasBackupDiffChanges(diff.summary)
|
|
605
|
+
? ''
|
|
606
|
+
: `
|
|
607
|
+
<div class="mt-5 border border-outline-variant/10 bg-surface-container px-4 py-5 text-center">
|
|
608
|
+
<span class="material-symbols-outlined mb-2 text-4xl text-primary-container/60">check_circle</span>
|
|
609
|
+
<div class="font-body text-sm font-black uppercase text-on-surface">No changes found</div>
|
|
610
|
+
<p class="mt-2 text-sm leading-6 text-on-surface-variant/65">
|
|
611
|
+
The verified backup and current database match.
|
|
612
|
+
</p>
|
|
613
|
+
</div>
|
|
614
|
+
`
|
|
615
|
+
}
|
|
616
|
+
${renderBackupDiffTabs(activeTab)}
|
|
617
|
+
${activeTab === 'schema' ? renderBackupDiffSchema(diff) : renderBackupDiffData(diff)}
|
|
618
|
+
</div>
|
|
619
|
+
`;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
export function renderBackupDiffDrawer(state) {
|
|
623
|
+
const diffState = state.backups.diff;
|
|
624
|
+
|
|
625
|
+
if (!diffState?.visible) {
|
|
626
|
+
return '';
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const title = diffState.data?.backup?.name ?? diffState.backupName ?? 'Backup comparison';
|
|
630
|
+
const currentLabel = diffState.data?.current?.label ?? diffState.currentLabel ?? 'Current database';
|
|
631
|
+
|
|
632
|
+
return `
|
|
633
|
+
<section class="flex h-full min-h-0 flex-col bg-surface-low">
|
|
634
|
+
<div class="border-b border-outline-variant/10 px-5 py-4">
|
|
635
|
+
<div class="flex items-center justify-between gap-3">
|
|
636
|
+
<div class="min-w-0">
|
|
637
|
+
<div class="font-mono text-[10px] uppercase tracking-[0.18em] text-primary-container/70">
|
|
638
|
+
Backup Compare
|
|
639
|
+
</div>
|
|
640
|
+
<h2 class="mt-1 truncate font-body text-lg font-black uppercase tracking-tight text-on-surface">
|
|
641
|
+
${escapeHtml(title)}
|
|
642
|
+
</h2>
|
|
643
|
+
<div class="mt-1 truncate text-xs text-on-surface-variant/60">
|
|
644
|
+
Current // ${escapeHtml(currentLabel)}
|
|
645
|
+
</div>
|
|
646
|
+
</div>
|
|
647
|
+
<button
|
|
648
|
+
class="query-history-icon-button"
|
|
649
|
+
data-action="close-backup-diff-drawer"
|
|
650
|
+
title="Close backup comparison"
|
|
651
|
+
type="button"
|
|
652
|
+
>
|
|
653
|
+
<span class="material-symbols-outlined text-[18px]">close</span>
|
|
654
|
+
</button>
|
|
655
|
+
</div>
|
|
656
|
+
<div class="mt-4 flex flex-wrap gap-2">
|
|
657
|
+
${renderStatusBadge('read only', 'muted')}
|
|
658
|
+
${diffState.loading ? renderStatusBadge('loading', 'muted') : ''}
|
|
659
|
+
${diffState.error ? renderStatusBadge('error', 'alert') : ''}
|
|
660
|
+
</div>
|
|
661
|
+
</div>
|
|
662
|
+
${renderBackupDiffDrawerBody(diffState)}
|
|
663
|
+
</section>
|
|
664
|
+
`;
|
|
665
|
+
}
|
|
666
|
+
|
|
147
667
|
export function renderBackupsView(state) {
|
|
148
668
|
const activeLabel = state.connections.active?.label ?? 'No active database';
|
|
149
669
|
const actions = `
|
|
@@ -171,6 +691,6 @@ export function renderBackupsView(state) {
|
|
|
171
691
|
</div>
|
|
172
692
|
</section>
|
|
173
693
|
`,
|
|
174
|
-
panel:
|
|
694
|
+
panel: renderBackupDiffDrawer(state),
|
|
175
695
|
};
|
|
176
696
|
}
|