sqlite-hub 1.1.0 → 1.1.2
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 +17 -196
- package/bin/sqlite-hub.js +7 -2
- package/frontend/js/api.js +60 -0
- package/frontend/js/app.js +140 -10
- package/frontend/js/components/dropdownButton.js +92 -0
- package/frontend/js/components/modal.js +991 -876
- package/frontend/js/components/queryEditor.js +29 -18
- package/frontend/js/components/queryHistoryDetail.js +20 -1
- package/frontend/js/components/queryHistoryHeader.js +3 -2
- package/frontend/js/components/rowEditorPanel.js +1 -0
- package/frontend/js/components/sidebar.js +1 -0
- package/frontend/js/components/structureGraph.js +1 -0
- package/frontend/js/components/tableDesignerEditor.js +23 -42
- package/frontend/js/components/tableDesignerSidebar.js +7 -31
- package/frontend/js/components/tableDesignerSqlPreview.js +2 -1
- package/frontend/js/router.js +2 -0
- package/frontend/js/store.js +407 -38
- package/frontend/js/utils/riskySql.js +165 -0
- package/frontend/js/views/backups.js +176 -0
- package/frontend/js/views/charts.js +12 -11
- package/frontend/js/views/data.js +37 -35
- package/frontend/js/views/documents.js +231 -162
- package/frontend/js/views/mediaTagging.js +6 -5
- package/frontend/js/views/structure.js +47 -43
- package/frontend/js/views/tableDesigner.js +45 -1
- package/frontend/styles/components.css +237 -59
- package/frontend/styles/structure-graph.css +10 -2
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/tokens.css +2 -0
- package/frontend/styles/views.css +84 -41
- package/package.json +2 -1
- package/server/routes/backups.js +120 -0
- package/server/routes/connections.js +26 -3
- package/server/routes/externalApi.js +6 -2
- package/server/server.js +3 -1
- package/server/services/databaseCommandService.js +4 -3
- package/server/services/sqlite/backupService.js +497 -66
- package/server/services/sqlite/connectionManager.js +2 -2
- package/server/services/sqlite/dataBrowserService.js +11 -3
- package/server/services/sqlite/importService.js +25 -0
- package/server/services/sqlite/sqlExecutor.js +2 -0
- package/server/services/storage/appStateStore.js +379 -88
- package/tests/api-token-auth.test.js +45 -2
- package/tests/backup-manager.test.js +140 -0
- package/tests/backups-view.test.js +64 -0
- package/tests/charts-height-preset-storage.test.js +60 -0
- package/tests/charts-route-state.test.js +144 -0
- package/tests/cli-service-delegation.test.js +97 -0
- package/tests/connection-removal.test.js +52 -0
- package/tests/database-command-service.test.js +38 -3
- package/tests/documents-view.test.js +132 -0
- package/tests/dropdown-button.test.js +75 -0
- package/tests/query-editor.test.js +28 -0
- package/tests/query-history-detail.test.js +37 -0
- package/tests/query-history-header.test.js +30 -0
- package/tests/risky-sql.test.js +30 -0
- package/tests/row-editor-null-values.test.js +24 -0
- package/tests/structure-view.test.js +56 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
const RISKY_PATTERNS = [
|
|
2
|
+
{ type: 'drop_table', pattern: /^\s*DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:"([^"]+)"|`([^`]+)`|\[([^\]]+)\]|([^\s;]+))/i },
|
|
3
|
+
{ type: 'schema_change', pattern: /^\s*ALTER\s+TABLE\b/i },
|
|
4
|
+
{ type: 'schema_change', pattern: /^\s*CREATE\s+(?:TABLE|INDEX|VIEW|TRIGGER)\b/i },
|
|
5
|
+
{ type: 'schema_change', pattern: /^\s*DROP\s+(?:INDEX|VIEW|TRIGGER)\b/i },
|
|
6
|
+
{ type: 'schema_change', pattern: /^\s*REINDEX\b/i },
|
|
7
|
+
{ type: 'schema_change', pattern: /^\s*VACUUM\b/i },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
function stripSqlComments(sql = '') {
|
|
11
|
+
const text = String(sql ?? '');
|
|
12
|
+
let output = '';
|
|
13
|
+
let index = 0;
|
|
14
|
+
let quote = null;
|
|
15
|
+
|
|
16
|
+
while (index < text.length) {
|
|
17
|
+
const char = text[index];
|
|
18
|
+
const next = text[index + 1];
|
|
19
|
+
|
|
20
|
+
if (quote) {
|
|
21
|
+
output += char;
|
|
22
|
+
if (char === quote) {
|
|
23
|
+
if (text[index + 1] === quote) {
|
|
24
|
+
output += text[index + 1];
|
|
25
|
+
index += 2;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
quote = null;
|
|
29
|
+
}
|
|
30
|
+
index += 1;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (char === "'" || char === '"' || char === '`') {
|
|
35
|
+
quote = char;
|
|
36
|
+
output += char;
|
|
37
|
+
index += 1;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (char === '-' && next === '-') {
|
|
42
|
+
while (index < text.length && text[index] !== '\n') {
|
|
43
|
+
index += 1;
|
|
44
|
+
}
|
|
45
|
+
output += '\n';
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (char === '/' && next === '*') {
|
|
50
|
+
index += 2;
|
|
51
|
+
while (index < text.length && !(text[index] === '*' && text[index + 1] === '/')) {
|
|
52
|
+
index += 1;
|
|
53
|
+
}
|
|
54
|
+
index += 2;
|
|
55
|
+
output += ' ';
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
output += char;
|
|
60
|
+
index += 1;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return output;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function splitSqlStatements(sql = '') {
|
|
67
|
+
const statements = [];
|
|
68
|
+
let current = '';
|
|
69
|
+
let quote = null;
|
|
70
|
+
|
|
71
|
+
for (let index = 0; index < sql.length; index += 1) {
|
|
72
|
+
const char = sql[index];
|
|
73
|
+
|
|
74
|
+
if (quote) {
|
|
75
|
+
current += char;
|
|
76
|
+
if (char === quote) {
|
|
77
|
+
if (sql[index + 1] === quote) {
|
|
78
|
+
current += sql[index + 1];
|
|
79
|
+
index += 1;
|
|
80
|
+
} else {
|
|
81
|
+
quote = null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (char === "'" || char === '"' || char === '`') {
|
|
88
|
+
quote = char;
|
|
89
|
+
current += char;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (char === ';') {
|
|
94
|
+
if (current.trim()) {
|
|
95
|
+
statements.push(current.trim());
|
|
96
|
+
}
|
|
97
|
+
current = '';
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
current += char;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (current.trim()) {
|
|
105
|
+
statements.push(current.trim());
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return statements;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function extractDropTableName(match) {
|
|
112
|
+
return [match?.[1], match?.[2], match?.[3], match?.[4]]
|
|
113
|
+
.map(value => String(value ?? '').trim())
|
|
114
|
+
.find(Boolean) ?? null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function detectRiskySqlOperations(sql = '') {
|
|
118
|
+
const statements = splitSqlStatements(stripSqlComments(sql));
|
|
119
|
+
const operations = [];
|
|
120
|
+
|
|
121
|
+
statements.forEach((statement, index) => {
|
|
122
|
+
for (const rule of RISKY_PATTERNS) {
|
|
123
|
+
const match = statement.match(rule.pattern);
|
|
124
|
+
if (!match) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
operations.push({
|
|
129
|
+
type: statements.length > 1 && rule.type !== 'drop_table' ? 'migration' : rule.type,
|
|
130
|
+
statement,
|
|
131
|
+
statementIndex: index,
|
|
132
|
+
tableName: rule.type === 'drop_table' ? extractDropTableName(match) : null,
|
|
133
|
+
});
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return operations;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function buildRiskySqlBackupContext(operations = []) {
|
|
142
|
+
const dropTable = operations.find(operation => operation.type === 'drop_table');
|
|
143
|
+
|
|
144
|
+
if (dropTable) {
|
|
145
|
+
return {
|
|
146
|
+
type: 'pre_schema_change',
|
|
147
|
+
name: `Before dropping table ${dropTable.tableName ?? ''}`.trim(),
|
|
148
|
+
description: `DROP TABLE may remove data permanently. ${dropTable.tableName ? `Target: ${dropTable.tableName}.` : ''}`.trim(),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (operations.some(operation => operation.type === 'migration') || operations.length > 1) {
|
|
153
|
+
return {
|
|
154
|
+
type: 'pre_migration',
|
|
155
|
+
name: 'Before migration',
|
|
156
|
+
description: 'Multiple schema-affecting statements were detected.',
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
type: 'pre_schema_change',
|
|
162
|
+
name: 'Before schema change',
|
|
163
|
+
description: 'This SQL can change database schema objects.',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { renderStatusBadge } from '../components/badges.js';
|
|
2
|
+
import { renderPageHeader } from '../components/pageHeader.js';
|
|
3
|
+
import { escapeHtml, formatBytes, formatCompactDateTime, truncateMiddle } from '../utils/format.js';
|
|
4
|
+
|
|
5
|
+
const STATUS_TONE = {
|
|
6
|
+
creating: 'muted',
|
|
7
|
+
verifying: 'muted',
|
|
8
|
+
verified: 'success',
|
|
9
|
+
failed: 'alert',
|
|
10
|
+
restoring: 'muted',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function formatStatus(value) {
|
|
14
|
+
return String(value ?? 'unknown')
|
|
15
|
+
.replace(/_/g, ' ')
|
|
16
|
+
.replace(/\b\w/g, letter => letter.toUpperCase());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function renderBackupStatus(backup) {
|
|
20
|
+
if (!backup.fileExists) {
|
|
21
|
+
return renderStatusBadge('File Missing', 'alert');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return renderStatusBadge(formatStatus(backup.status), STATUS_TONE[backup.status] ?? 'muted');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isBackupBusy(backup, state) {
|
|
28
|
+
return Boolean(state.backups.operationLoading) || ['creating', 'verifying', 'restoring'].includes(backup.status);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function renderBackupRows(state) {
|
|
32
|
+
return state.backups.items
|
|
33
|
+
.map(backup => {
|
|
34
|
+
const busy = isBackupBusy(backup, state);
|
|
35
|
+
const canRestore = backup.status === 'verified' && backup.fileExists && !busy && !state.connections.active?.readOnly;
|
|
36
|
+
const canDownload = backup.fileExists && !busy;
|
|
37
|
+
const canEditNotes = !busy;
|
|
38
|
+
const canDelete = !busy;
|
|
39
|
+
|
|
40
|
+
return `
|
|
41
|
+
<tr class="border-b border-outline-variant/10 align-top">
|
|
42
|
+
<td class="px-4 py-4 text-xs font-mono text-on-surface/80">${escapeHtml(formatCompactDateTime(backup.createdAt))}</td>
|
|
43
|
+
<td class="px-4 py-4">
|
|
44
|
+
<div class="font-headline text-sm font-black uppercase text-on-surface">${escapeHtml(backup.name)}</div>
|
|
45
|
+
<div class="mt-1 font-mono text-[10px] uppercase tracking-[0.12em] text-on-surface-variant/45" title="${escapeHtml(backup.path)}">
|
|
46
|
+
${escapeHtml(truncateMiddle(backup.fileName || backup.path, 44))}
|
|
47
|
+
</div>
|
|
48
|
+
</td>
|
|
49
|
+
<td class="px-4 py-4 text-right text-xs font-mono text-on-surface/70">${escapeHtml(formatBytes(backup.sizeBytes))}</td>
|
|
50
|
+
<td class="px-4 py-4">${renderBackupStatus(backup)}</td>
|
|
51
|
+
<td class="max-w-[18rem] px-4 py-4 text-xs leading-6 text-on-surface-variant/70">
|
|
52
|
+
${backup.notes ? escapeHtml(backup.notes) : '<span class="text-on-surface-variant/35">-</span>'}
|
|
53
|
+
${backup.errorMessage ? `<div class="mt-2 text-error">${escapeHtml(backup.errorMessage)}</div>` : ''}
|
|
54
|
+
<div class="mt-3">
|
|
55
|
+
<button class="standard-button" data-action="open-edit-backup-notes-modal" data-backup-id="${escapeHtml(backup.id)}" type="button" ${canEditNotes ? '' : 'disabled'}>
|
|
56
|
+
<span class="material-symbols-outlined text-sm">edit_note</span>
|
|
57
|
+
Edit Notes
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
</td>
|
|
61
|
+
<td class="px-4 py-4">
|
|
62
|
+
<div class="flex flex-wrap items-center justify-end gap-2">
|
|
63
|
+
<button class="standard-button" data-action="open-restore-backup-modal" data-backup-id="${escapeHtml(backup.id)}" type="button" ${canRestore ? '' : 'disabled'}>
|
|
64
|
+
<span class="material-symbols-outlined text-sm">restore</span>
|
|
65
|
+
Restore
|
|
66
|
+
</button>
|
|
67
|
+
<button class="standard-button" data-action="download-backup" data-backup-id="${escapeHtml(backup.id)}" type="button" ${canDownload ? '' : 'disabled'}>
|
|
68
|
+
<span class="material-symbols-outlined text-sm">download</span>
|
|
69
|
+
Download
|
|
70
|
+
</button>
|
|
71
|
+
<button class="delete-button" data-action="open-delete-backup-modal" data-backup-id="${escapeHtml(backup.id)}" type="button" ${canDelete ? '' : 'disabled'}>
|
|
72
|
+
<span class="material-symbols-outlined text-sm">delete</span>
|
|
73
|
+
Delete
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
76
|
+
</td>
|
|
77
|
+
</tr>
|
|
78
|
+
`;
|
|
79
|
+
})
|
|
80
|
+
.join('');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function renderBackupsBody(state) {
|
|
84
|
+
if (state.backups.loading) {
|
|
85
|
+
return `
|
|
86
|
+
<div class="flex min-h-[280px] items-center justify-center border border-outline-variant/10 bg-surface-container-low">
|
|
87
|
+
<div class="text-center text-on-surface-variant/40">
|
|
88
|
+
<span class="material-symbols-outlined mb-3 text-4xl">progress_activity</span>
|
|
89
|
+
<p class="font-mono text-[10px] uppercase tracking-[0.22em]">LOADING_BACKUPS</p>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (state.backups.error) {
|
|
96
|
+
return `
|
|
97
|
+
<div class="border border-error/20 bg-error-container/10 px-6 py-5 text-sm text-on-surface">
|
|
98
|
+
<div class="font-headline text-xs font-bold uppercase tracking-[0.18em] text-error">
|
|
99
|
+
${escapeHtml(state.backups.error.code)}
|
|
100
|
+
</div>
|
|
101
|
+
<div class="mt-2">${escapeHtml(state.backups.error.message)}</div>
|
|
102
|
+
</div>
|
|
103
|
+
`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!state.backups.items.length) {
|
|
107
|
+
return `
|
|
108
|
+
<div class="border border-dashed border-outline-variant/20 bg-surface-container-low px-8 py-10 text-center">
|
|
109
|
+
<span class="material-symbols-outlined mb-3 text-5xl text-on-surface-variant/25">inventory_2</span>
|
|
110
|
+
<p class="font-headline text-xl font-black uppercase tracking-tight text-primary-container">
|
|
111
|
+
No backups yet
|
|
112
|
+
</p>
|
|
113
|
+
<p class="mx-auto mt-3 max-w-xl text-sm leading-7 text-on-surface-variant/65">
|
|
114
|
+
Create a backup to protect your database before making significant changes.
|
|
115
|
+
</p>
|
|
116
|
+
<div class="mt-6 flex items-center justify-center">
|
|
117
|
+
<button class="signature-button" data-action="open-create-backup-modal" type="button">
|
|
118
|
+
<span class="material-symbols-outlined text-sm">inventory_2</span>
|
|
119
|
+
Create backup
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return `
|
|
127
|
+
<div class="overflow-auto border border-outline-variant/10 bg-surface-container-low">
|
|
128
|
+
<table class="min-w-full border-collapse text-left">
|
|
129
|
+
<thead class="border-b border-outline-variant/10 bg-surface-container">
|
|
130
|
+
<tr class="font-mono text-[10px] uppercase tracking-[0.18em] text-on-surface-variant/55">
|
|
131
|
+
<th class="px-4 py-3 font-normal">Created</th>
|
|
132
|
+
<th class="px-4 py-3 font-normal">Name</th>
|
|
133
|
+
<th class="px-4 py-3 text-right font-normal">Size</th>
|
|
134
|
+
<th class="px-4 py-3 font-normal">Status</th>
|
|
135
|
+
<th class="px-4 py-3 font-normal">Note</th>
|
|
136
|
+
<th class="px-4 py-3 text-right font-normal">Actions</th>
|
|
137
|
+
</tr>
|
|
138
|
+
</thead>
|
|
139
|
+
<tbody>
|
|
140
|
+
${renderBackupRows(state)}
|
|
141
|
+
</tbody>
|
|
142
|
+
</table>
|
|
143
|
+
</div>
|
|
144
|
+
`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function renderBackupsView(state) {
|
|
148
|
+
const activeLabel = state.connections.active?.label ?? 'No active database';
|
|
149
|
+
const actions = `
|
|
150
|
+
<button class="standard-button" data-action="refresh-backups" type="button" ${state.backups.loading ? 'disabled' : ''}>
|
|
151
|
+
<span class="material-symbols-outlined text-sm">refresh</span>
|
|
152
|
+
Refresh
|
|
153
|
+
</button>
|
|
154
|
+
<button class="signature-button" data-action="open-create-backup-modal" type="button" ${state.backups.operationLoading ? 'disabled' : ''}>
|
|
155
|
+
<span class="material-symbols-outlined text-sm">inventory_2</span>
|
|
156
|
+
Create backup
|
|
157
|
+
</button>
|
|
158
|
+
`;
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
main: `
|
|
162
|
+
<section class="view-surface relative min-h-full overflow-hidden">
|
|
163
|
+
<div class="data-grid-texture pointer-events-none absolute inset-0"></div>
|
|
164
|
+
<div class="view-frame relative z-10">
|
|
165
|
+
${renderPageHeader({
|
|
166
|
+
title: 'Backups',
|
|
167
|
+
subtitle: `Active // ${activeLabel}`,
|
|
168
|
+
actions,
|
|
169
|
+
})}
|
|
170
|
+
${renderBackupsBody(state)}
|
|
171
|
+
</div>
|
|
172
|
+
</section>
|
|
173
|
+
`,
|
|
174
|
+
panel: '',
|
|
175
|
+
};
|
|
176
|
+
}
|
|
@@ -170,10 +170,10 @@ function renderChartsList(state) {
|
|
|
170
170
|
hasSearch
|
|
171
171
|
? 'Try another title, SQL fragment, chart type, or saved status.'
|
|
172
172
|
: isSavedTab
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
173
|
+
? 'Save chartable queries from this list or from the SQL Editor to keep them here.'
|
|
174
|
+
: isUnsavedTab
|
|
175
|
+
? 'Unsaved chartable queries will appear here until you bookmark them.'
|
|
176
|
+
: 'Run SELECT queries in the SQL Editor first. They will appear here automatically.'
|
|
177
177
|
}
|
|
178
178
|
</p>
|
|
179
179
|
</div>
|
|
@@ -549,7 +549,7 @@ function renderChartCard(chart, state, analysis) {
|
|
|
549
549
|
'<div class="flex flex-wrap items-center gap-2">',
|
|
550
550
|
'<h3 class="truncate font-headline text-xl font-black uppercase tracking-tight text-on-surface">',
|
|
551
551
|
escapeHtml(chart.name),
|
|
552
|
-
|
|
552
|
+
'</h3>',
|
|
553
553
|
renderStatusBadge(getQueryChartTypeLabel(chart.chartType), 'primary'),
|
|
554
554
|
'</div></div><div class="flex flex-wrap items-center gap-2">',
|
|
555
555
|
'<button class="standard-button" data-action="open-edit-query-chart-modal" data-chart-id="',
|
|
@@ -557,14 +557,14 @@ function renderChartCard(chart, state, analysis) {
|
|
|
557
557
|
'" type="button">Edit</button>',
|
|
558
558
|
'<button class="delete-button" data-action="open-delete-query-chart-modal" data-chart-id="',
|
|
559
559
|
escapeHtml(chart.id),
|
|
560
|
-
'" type="button">Delete</button>',
|
|
560
|
+
'" type="button"><span class="material-symbols-outlined">delete</span> Delete</button>',
|
|
561
561
|
'<button class="standard-button" data-action="export-query-chart-png" data-chart-id="',
|
|
562
562
|
escapeHtml(chart.id),
|
|
563
563
|
'" type="button">Export PNG</button>',
|
|
564
564
|
'</div></header><div class="query-chart-card__body">',
|
|
565
565
|
renderChartSurface(chart, state, analysis),
|
|
566
|
-
|
|
567
|
-
].join(
|
|
566
|
+
'</div></article>',
|
|
567
|
+
].join('');
|
|
568
568
|
}
|
|
569
569
|
|
|
570
570
|
export function renderChartsDetail(state) {
|
|
@@ -636,15 +636,16 @@ export function renderChartsDetail(state) {
|
|
|
636
636
|
</div>
|
|
637
637
|
<div class="charts-detail-shell__controls-group charts-detail-shell__controls-group--end">
|
|
638
638
|
<button
|
|
639
|
-
class="standard-button"
|
|
639
|
+
class="standard-button panel-toggle-button ${historyVisible ? '' : 'is-active'}"
|
|
640
|
+
aria-pressed="${historyVisible ? 'false' : 'true'}"
|
|
640
641
|
data-action="toggle-query-history-panel"
|
|
641
642
|
data-next-value="${historyVisible ? 'false' : 'true'}"
|
|
642
643
|
type="button"
|
|
643
644
|
>
|
|
644
645
|
<span class="material-symbols-outlined text-sm">${
|
|
645
|
-
|
|
646
|
+
historyVisible ? 'visibility_off' : 'visibility'
|
|
646
647
|
}</span>
|
|
647
|
-
${historyVisible ? 'Hide
|
|
648
|
+
${historyVisible ? 'Hide History' : 'Show History'}
|
|
648
649
|
</button>
|
|
649
650
|
</div>
|
|
650
651
|
</div>
|
|
@@ -78,31 +78,32 @@ function renderTableList(state) {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
return `
|
|
81
|
-
<div class="custom-scrollbar
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
>
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
81
|
+
<div class="subnavi-list custom-scrollbar">
|
|
82
|
+
${filteredTables
|
|
83
|
+
.map(
|
|
84
|
+
table => `
|
|
85
|
+
<button
|
|
86
|
+
class="table-designer-sidebar__item subnavi-item border px-4 py-3 text-left transition-colors ${
|
|
87
|
+
table.name === activeName
|
|
88
|
+
? 'is-active border-primary-container/30 bg-surface-container-high'
|
|
89
|
+
: 'border-outline-variant/10 bg-surface-container-lowest hover:bg-surface-container-high'
|
|
90
|
+
}"
|
|
91
|
+
data-action="navigate"
|
|
92
|
+
data-to="/data/${encodeURIComponent(table.name)}"
|
|
93
|
+
type="button"
|
|
94
|
+
>
|
|
95
|
+
<div class="table-designer-sidebar__item-name ${table.name === activeName ? 'is-active' : ''}">
|
|
96
|
+
${escapeHtml(table.name)}
|
|
97
|
+
</div>
|
|
98
|
+
<div class="mt-1 truncate text-[10px] uppercase tracking-[0.16em] text-on-surface-variant/45">
|
|
99
|
+
${escapeHtml(formatNumber(table.columnCount ?? 0))} column${
|
|
100
|
+
Number(table.columnCount ?? 0) === 1 ? '' : 's'
|
|
101
|
+
}
|
|
102
|
+
</div>
|
|
103
|
+
</button>
|
|
104
|
+
`,
|
|
105
|
+
)
|
|
106
|
+
.join('')}
|
|
106
107
|
</div>
|
|
107
108
|
`;
|
|
108
109
|
}
|
|
@@ -112,13 +113,14 @@ function renderWorkspaceHeader(state) {
|
|
|
112
113
|
const tablesVisible = state.dataBrowser.tablesVisible !== false;
|
|
113
114
|
|
|
114
115
|
return `
|
|
115
|
-
<header class="
|
|
116
|
+
<header class="workspace-header">
|
|
116
117
|
<div class="flex flex-wrap items-center justify-between gap-4">
|
|
117
118
|
<div class="flex items-center gap-3">
|
|
118
119
|
${
|
|
119
120
|
table
|
|
120
121
|
? `<button
|
|
121
|
-
class="standard-button"
|
|
122
|
+
class="standard-button panel-toggle-button ${tablesVisible ? '' : 'is-active'}"
|
|
123
|
+
aria-pressed="${tablesVisible ? 'false' : 'true'}"
|
|
122
124
|
data-action="toggle-data-tables"
|
|
123
125
|
type="button"
|
|
124
126
|
>
|
|
@@ -631,17 +633,17 @@ export function renderDataView(state) {
|
|
|
631
633
|
return {
|
|
632
634
|
main: `
|
|
633
635
|
<section class="view-surface min-h-full bg-surface-container flex h-full min-h-0 flex-col overflow-hidden">
|
|
634
|
-
<div class="
|
|
636
|
+
<div class="data-view-grid ${tablesVisible ? 'data-view-grid--with-subnavi' : ''}">
|
|
635
637
|
${
|
|
636
638
|
tablesVisible
|
|
637
639
|
? `
|
|
638
|
-
<aside class="
|
|
639
|
-
<div class="
|
|
640
|
-
<div
|
|
641
|
-
Tables
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
640
|
+
<aside class="data-view__sidebar subnavi-panel">
|
|
641
|
+
<div class="subnavi-header">
|
|
642
|
+
<div>
|
|
643
|
+
<div class="subnavi-header-title">Tables</div>
|
|
644
|
+
<div class="subnavi-header-details">
|
|
645
|
+
total ${escapeHtml(formatNumber(state.dataBrowser.tables.length))}
|
|
646
|
+
</div>
|
|
645
647
|
</div>
|
|
646
648
|
</div>
|
|
647
649
|
${renderDataTableSearch(state)}
|