sqlite-hub 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/bin/sqlite-hub.js +555 -122
- package/docs/API.md +30 -0
- package/docs/CLI.md +22 -0
- package/docs/CLI_API_PARITY.md +61 -0
- package/docs/changelog.md +9 -1
- package/docs/guidelines/AGENTS.md +32 -0
- package/docs/{DESIGN_GUIDELINES.md → guidelines/DESIGN.md} +10 -0
- package/docs/todo.md +1 -13
- package/frontend/js/api.js +45 -0
- package/frontend/js/app.js +192 -2
- package/frontend/js/components/connectionCard.js +3 -1
- package/frontend/js/components/modal.js +356 -15
- package/frontend/js/components/sidebar.js +41 -7
- package/frontend/js/components/topNav.js +2 -14
- package/frontend/js/router.js +10 -0
- package/frontend/js/store.js +448 -0
- package/frontend/js/utils/inputClear.js +36 -0
- package/frontend/js/utils/syntheticData.js +240 -0
- package/frontend/js/views/backups.js +52 -0
- package/frontend/js/views/data.js +16 -0
- package/frontend/js/views/logs.js +339 -0
- package/frontend/js/views/settings.js +77 -27
- package/frontend/js/views/structure.js +6 -0
- package/frontend/js/views/tableAdvisor.js +385 -0
- package/frontend/js/views/tableDesigner.js +6 -0
- package/frontend/styles/components.css +149 -0
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/views.css +8 -0
- package/package.json +1 -1
- package/server/routes/data.js +45 -0
- package/server/routes/externalApi.js +285 -2
- package/server/routes/logs.js +127 -0
- package/server/server.js +3 -0
- package/server/services/databaseCommandService.js +45 -0
- package/server/services/sqlite/dataBrowserService.js +36 -0
- package/server/services/sqlite/introspection.js +226 -22
- package/server/services/sqlite/syntheticDataGenerator.js +728 -0
- package/server/services/sqlite/tableAdvisor.js +790 -0
- package/server/services/storage/appStateStore.js +773 -169
|
@@ -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 `
|
|
@@ -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">
|
|
@@ -248,6 +248,12 @@ function renderGraphSurface(structure, selectedName, detail, detailLoading, tabl
|
|
|
248
248
|
label: 'Table Designer',
|
|
249
249
|
target: tableName => `/table-designer/${encodeURIComponent(tableName)}`,
|
|
250
250
|
},
|
|
251
|
+
{
|
|
252
|
+
icon: 'troubleshoot',
|
|
253
|
+
key: 'table-advisor',
|
|
254
|
+
label: 'Table Advisor',
|
|
255
|
+
target: tableName => `/table-advisor/${encodeURIComponent(tableName)}`,
|
|
256
|
+
},
|
|
251
257
|
{
|
|
252
258
|
key: 'sql-editor',
|
|
253
259
|
},
|
|
@@ -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
|
},
|