@lunora/studio 0.0.0 → 1.0.0-alpha.1
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/LICENSE.md +105 -0
- package/README.md +123 -9
- package/__assets__/package-og.svg +14 -0
- package/dist/index.d.ts +1402 -0
- package/dist/index.js +41 -0
- package/dist/mount.d.ts +21 -0
- package/dist/mount.js +26 -0
- package/dist/packem_shared/ADMIN_FUNCTION_PREFIX-DmBqMZ-z.js +45 -0
- package/dist/packem_shared/ApiDocsPanel-DpRjJhG5.js +842 -0
- package/dist/packem_shared/ApiReferencePanel-DMIUp-kK.js +229 -0
- package/dist/packem_shared/ApiTab-DURGU15e.js +251 -0
- package/dist/packem_shared/AuditPanel-BC59Nhst.js +212 -0
- package/dist/packem_shared/CommandPalette-Dx_CoB9i.js +373 -0
- package/dist/packem_shared/ConfirmButton-WQVUoGFb.js +59 -0
- package/dist/packem_shared/ConnectionBadge-Bxagrip8.js +111 -0
- package/dist/packem_shared/DEFAULT_AUTO_REFRESH_MS-Vxwaxx51.js +50 -0
- package/dist/packem_shared/DEFAULT_INSIGHT_THRESHOLDS-DjF0h-gA.js +89 -0
- package/dist/packem_shared/DataBrowser-Coz6jJE6.js +4542 -0
- package/dist/packem_shared/DataFilters-FNquMaiu.js +249 -0
- package/dist/packem_shared/ErrorBoundary-BzAApI7J.js +66 -0
- package/dist/packem_shared/ExportImportPanel-WO34fJxy.js +193 -0
- package/dist/packem_shared/FileBrowser-Zcr-Qgxo.js +2932 -0
- package/dist/packem_shared/FunctionRunner-j0Rd5m9t.js +343 -0
- package/dist/packem_shared/FunctionStatsPanel-DboBl-XL.js +432 -0
- package/dist/packem_shared/GlobalDataBrowser-9MhPEfgN.js +318 -0
- package/dist/packem_shared/HealthPanel-DOIgbUtx.js +640 -0
- package/dist/packem_shared/HomePanel-bdOCNA-p.js +1273 -0
- package/dist/packem_shared/InsightsPanel-DaZPnSgt.js +423 -0
- package/dist/packem_shared/LogsPanel-CWdqAGpQ.js +839 -0
- package/dist/packem_shared/MailPanel-D_EGtDnS.js +447 -0
- package/dist/packem_shared/MetricsPanel-E4Gv6wTO.js +1625 -0
- package/dist/packem_shared/MigrationsPanel-DQdPY9io.js +246 -0
- package/dist/packem_shared/OpenRpcReferencePanel-j2p3HB0s.js +191 -0
- package/dist/packem_shared/PitrPanel-BbBkQR6t.js +252 -0
- package/dist/packem_shared/STUDIO_ROOT_CLASS-D12gX2dV.js +3 -0
- package/dist/packem_shared/ScheduledJobs-Ok1CYYwI.js +159 -0
- package/dist/packem_shared/SchemaViewer-D8XGnp-X.js +2512 -0
- package/dist/packem_shared/SecurityAdvisorPanel-Cdm2IxLW.js +79 -0
- package/dist/packem_shared/SettingsPanel-D3WF2mBU.js +176 -0
- package/dist/packem_shared/ShardInput-DNCsT1KW.js +107 -0
- package/dist/packem_shared/SqlEditorPanel-BuQ7f2Hs.js +13 -0
- package/dist/packem_shared/Studio-D36od9Oz.js +33 -0
- package/dist/packem_shared/StudioApp-dvywkJ8I.js +383 -0
- package/dist/packem_shared/StudioI18nProvider-Dcajsznk.js +48 -0
- package/dist/packem_shared/TableEditor-DIVDk3vT.js +371 -0
- package/dist/packem_shared/advisor-view-DBlzJi6C.js +159 -0
- package/dist/packem_shared/aggregateMetrics-D4nUHEKU.js +108 -0
- package/dist/packem_shared/app.d-CCmwDEVs.d.ts +300 -0
- package/dist/packem_shared/badge-B2PKA1-5.js +49 -0
- package/dist/packem_shared/bar-chart-CzJAgqkp.js +3245 -0
- package/dist/packem_shared/button-BhsN2uZH.js +49 -0
- package/dist/packem_shared/card-DURq3ElK.js +175 -0
- package/dist/packem_shared/cf-links-BZfRdxSE.js +8 -0
- package/dist/packem_shared/checkbox-UNkzAxl-.js +63 -0
- package/dist/packem_shared/createStudioI18n-CgvlmDkN.js +27 -0
- package/dist/packem_shared/data-grid-CCh2Couo.js +183 -0
- package/dist/packem_shared/dropdown-menu-WY4B_eJO.js +280 -0
- package/dist/packem_shared/empty-state-DY_oe0k6.js +98 -0
- package/dist/packem_shared/grid-features-DTjG6Sex.js +840 -0
- package/dist/packem_shared/input-XH4r1Pt1.js +53 -0
- package/dist/packem_shared/internal-BBZYexre.js +68 -0
- package/dist/packem_shared/label-D8ykjn5J.js +46 -0
- package/dist/packem_shared/live-status-bPff1O7Y.js +44 -0
- package/dist/packem_shared/reference-view-BCKIoai7.js +2180 -0
- package/dist/packem_shared/shard-history-DyebH1R5.js +38 -0
- package/dist/packem_shared/sparkline-10dG-_f0.js +93 -0
- package/dist/packem_shared/sql-editor-panel-CW2y2x9h.js +2562 -0
- package/dist/packem_shared/storage-tier-CL98eOvn.js +85 -0
- package/dist/packem_shared/studio-BDVd7rIV.js +10303 -0
- package/dist/packem_shared/table-_RzNvy3R.js +246 -0
- package/dist/packem_shared/table-list-sidebar-aZHLq70w.js +832 -0
- package/dist/packem_shared/textarea-D3gaCU_-.js +46 -0
- package/dist/packem_shared/use-live-admin-D1h1Fzsd.js +73 -0
- package/dist/packem_shared/use-live-shard-seed-B74RYcOy.js +76 -0
- package/dist/packem_shared/useDebounced-Dxncpg6z.js +32 -0
- package/dist/packem_shared/utils-B05Dmz_H.js +8 -0
- package/dist/packem_shared/virtual-rect-CVMUskSm.js +10 -0
- package/dist/standalone/studio.js +356 -0
- package/dist/styles.css +2 -0
- package/package.json +77 -17
- package/src/theme.css +59 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import { c } from 'react/compiler-runtime';
|
|
2
|
+
import { useLunora } from '@lunora/react';
|
|
3
|
+
import { useNavigate } from '@tanstack/react-router';
|
|
4
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
5
|
+
import { ShardInput } from './ShardInput-DNCsT1KW.js';
|
|
6
|
+
import { B as Button } from './button-BhsN2uZH.js';
|
|
7
|
+
import { useT } from './createStudioI18n-CgvlmDkN.js';
|
|
8
|
+
import { ADMIN_FUNCTIONS } from './ADMIN_FUNCTION_PREFIX-DmBqMZ-z.js';
|
|
9
|
+
import { f as fireAndForget, c as callOptions, e as errorMessage, a as adminRef } from './internal-BBZYexre.js';
|
|
10
|
+
import { r as recordShard } from './shard-history-DyebH1R5.js';
|
|
11
|
+
import { u as useLiveShardSeed } from './use-live-shard-seed-B74RYcOy.js';
|
|
12
|
+
import { a as advisoryRow, A as AdvisorView } from './advisor-view-DBlzJi6C.js';
|
|
13
|
+
import { ConfirmButton } from './ConfirmButton-WQVUoGFb.js';
|
|
14
|
+
import { jsxDEV } from 'react/jsx-dev-runtime';
|
|
15
|
+
import { deriveInsights } from './DEFAULT_INSIGHT_THRESHOLDS-DjF0h-gA.js';
|
|
16
|
+
import { runAdvisor, RUNTIME_LINTS } from '@lunora/advisor';
|
|
17
|
+
|
|
18
|
+
const hasIndexMetadata = (metadata) => {
|
|
19
|
+
const {
|
|
20
|
+
suggestedIndex,
|
|
21
|
+
table
|
|
22
|
+
} = metadata;
|
|
23
|
+
if (typeof table !== "string" || table.length === 0) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
if (typeof suggestedIndex !== "object" || suggestedIndex === null || !Array.isArray(suggestedIndex["fields"]) || suggestedIndex["fields"].length === 0) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
};
|
|
31
|
+
const composeCreateIndex = (table, indexName, fields) => {
|
|
32
|
+
const quotedTable = `"${table}"`;
|
|
33
|
+
const quotedIndex = `"${indexName}"`;
|
|
34
|
+
const quotedColumns = fields.map((f) => `"${f}"`).join(", ");
|
|
35
|
+
return `CREATE INDEX IF NOT EXISTS ${quotedIndex} ON ${quotedTable} (${quotedColumns});`;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const ApplyIndexButton = (t0) => {
|
|
39
|
+
const $ = c(19);
|
|
40
|
+
const {
|
|
41
|
+
fields,
|
|
42
|
+
indexName,
|
|
43
|
+
table,
|
|
44
|
+
testId
|
|
45
|
+
} = t0;
|
|
46
|
+
const t = useT();
|
|
47
|
+
const [applied, setApplied] = useState(false);
|
|
48
|
+
let t1;
|
|
49
|
+
if ($[0] !== fields || $[1] !== indexName || $[2] !== table) {
|
|
50
|
+
t1 = () => {
|
|
51
|
+
const clipboard = "navigator" in globalThis ? globalThis.navigator.clipboard : void 0;
|
|
52
|
+
if (clipboard === void 0) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const sql = composeCreateIndex(table, indexName, fields);
|
|
56
|
+
fireAndForget(clipboard.writeText(sql));
|
|
57
|
+
setApplied(true);
|
|
58
|
+
};
|
|
59
|
+
$[0] = fields;
|
|
60
|
+
$[1] = indexName;
|
|
61
|
+
$[2] = table;
|
|
62
|
+
$[3] = t1;
|
|
63
|
+
} else {
|
|
64
|
+
t1 = $[3];
|
|
65
|
+
}
|
|
66
|
+
const onConfirm = t1;
|
|
67
|
+
if (applied) {
|
|
68
|
+
const t22 = `${testId}-applied`;
|
|
69
|
+
let t32;
|
|
70
|
+
if ($[4] !== t) {
|
|
71
|
+
t32 = t("CREATE INDEX SQL copied to clipboard.");
|
|
72
|
+
$[4] = t;
|
|
73
|
+
$[5] = t32;
|
|
74
|
+
} else {
|
|
75
|
+
t32 = $[5];
|
|
76
|
+
}
|
|
77
|
+
let t42;
|
|
78
|
+
if ($[6] !== t22 || $[7] !== t32) {
|
|
79
|
+
t42 = /* @__PURE__ */ jsxDEV("span", {
|
|
80
|
+
className: "text-xs text-muted-foreground",
|
|
81
|
+
"data-testid": t22,
|
|
82
|
+
children: t32
|
|
83
|
+
}, void 0, false);
|
|
84
|
+
$[6] = t22;
|
|
85
|
+
$[7] = t32;
|
|
86
|
+
$[8] = t42;
|
|
87
|
+
} else {
|
|
88
|
+
t42 = $[8];
|
|
89
|
+
}
|
|
90
|
+
return t42;
|
|
91
|
+
}
|
|
92
|
+
let t2;
|
|
93
|
+
if ($[9] !== t) {
|
|
94
|
+
t2 = t("Apply?");
|
|
95
|
+
$[9] = t;
|
|
96
|
+
$[10] = t2;
|
|
97
|
+
} else {
|
|
98
|
+
t2 = $[10];
|
|
99
|
+
}
|
|
100
|
+
let t3;
|
|
101
|
+
if ($[11] !== t || $[12] !== table) {
|
|
102
|
+
t3 = t("Apply index on {table}", {
|
|
103
|
+
table
|
|
104
|
+
});
|
|
105
|
+
$[11] = t;
|
|
106
|
+
$[12] = table;
|
|
107
|
+
$[13] = t3;
|
|
108
|
+
} else {
|
|
109
|
+
t3 = $[13];
|
|
110
|
+
}
|
|
111
|
+
let t4;
|
|
112
|
+
if ($[14] !== onConfirm || $[15] !== t2 || $[16] !== t3 || $[17] !== testId) {
|
|
113
|
+
t4 = /* @__PURE__ */ jsxDEV(ConfirmButton, {
|
|
114
|
+
confirmLabel: t2,
|
|
115
|
+
onConfirm,
|
|
116
|
+
testId,
|
|
117
|
+
children: t3
|
|
118
|
+
}, void 0, false);
|
|
119
|
+
$[14] = onConfirm;
|
|
120
|
+
$[15] = t2;
|
|
121
|
+
$[16] = t3;
|
|
122
|
+
$[17] = testId;
|
|
123
|
+
$[18] = t4;
|
|
124
|
+
} else {
|
|
125
|
+
t4 = $[18];
|
|
126
|
+
}
|
|
127
|
+
return t4;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const reconcileIndexHits = (declaredIndexes, indexHits) => {
|
|
131
|
+
const readsByKey = /* @__PURE__ */ new Map();
|
|
132
|
+
for (const hit of indexHits) {
|
|
133
|
+
const key = `${hit.table} ${hit.index}`;
|
|
134
|
+
readsByKey.set(key, (readsByKey.get(key) ?? 0) + hit.reads);
|
|
135
|
+
}
|
|
136
|
+
return declaredIndexes.map((declared) => {
|
|
137
|
+
return {
|
|
138
|
+
index: declared.index,
|
|
139
|
+
reads: readsByKey.get(`${declared.table} ${declared.index}`) ?? 0,
|
|
140
|
+
table: declared.table
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
const aggregateTableScans = (functions) => {
|
|
145
|
+
const scansByTable = /* @__PURE__ */ new Map();
|
|
146
|
+
for (const callStat of functions) {
|
|
147
|
+
for (const attribution of callStat.scannedTables ?? []) {
|
|
148
|
+
scansByTable.set(attribution.table, (scansByTable.get(attribution.table) ?? 0) + attribution.scans);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return [...scansByTable].map(([table, scans]) => {
|
|
152
|
+
return {
|
|
153
|
+
scans,
|
|
154
|
+
table
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
const declaredIndexesFor = (table, indexes) => indexes.map((index) => {
|
|
159
|
+
return {
|
|
160
|
+
index: index.name,
|
|
161
|
+
table
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
const deriveRuntimeAdvisories = (inputs) => {
|
|
165
|
+
const inDoIndexHits = reconcileIndexHits(inputs.declaredIndexes ?? [], inputs.indexHits ?? []);
|
|
166
|
+
const inDoTableScans = aggregateTableScans(inputs.functions ?? []);
|
|
167
|
+
const inDoShardTraffic = inputs.shardTraffic ?? [];
|
|
168
|
+
const analytics = inputs.analyticsMetrics;
|
|
169
|
+
const indexHits = analytics && analytics.indexHits.length > 0 ? analytics.indexHits : inDoIndexHits;
|
|
170
|
+
const tableScans = analytics && analytics.tableScans.length > 0 ? analytics.tableScans : inDoTableScans;
|
|
171
|
+
const shardTraffic = analytics && analytics.shardTraffic.length > 0 ? analytics.shardTraffic : inDoShardTraffic;
|
|
172
|
+
const findings = runAdvisor({
|
|
173
|
+
indexHits,
|
|
174
|
+
schema: {
|
|
175
|
+
tables: []
|
|
176
|
+
},
|
|
177
|
+
shardTraffic,
|
|
178
|
+
tableScans
|
|
179
|
+
}, {
|
|
180
|
+
lints: RUNTIME_LINTS,
|
|
181
|
+
source: "runtime"
|
|
182
|
+
});
|
|
183
|
+
const suppress = inputs.suppressHotScanTables;
|
|
184
|
+
const visible = suppress === void 0 ? findings : findings.filter((finding) => !(finding.metadata["kind"] === "hot_scan" && typeof finding.metadata["table"] === "string" && suppress.has(finding.metadata["table"])));
|
|
185
|
+
return visible.map((finding) => advisoryRow(finding));
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const GET_ADVISORIES = adminRef(ADMIN_FUNCTIONS.getAdvisories);
|
|
189
|
+
const GET_FUNCTION_STATS = adminRef(ADMIN_FUNCTIONS.getFunctionStats);
|
|
190
|
+
const GET_METRICS = adminRef(ADMIN_FUNCTIONS.getMetrics);
|
|
191
|
+
const LIST_TABLES = adminRef(ADMIN_FUNCTIONS.listTables);
|
|
192
|
+
const LIST_TABLE_INDEXES = adminRef(ADMIN_FUNCTIONS.listTableIndexes);
|
|
193
|
+
const percent = (rate) => `${(rate * 100).toFixed(1)}%`;
|
|
194
|
+
const seconds = (ms) => `${(ms / 1e3).toFixed(2)}s`;
|
|
195
|
+
const tableList = (tables) => {
|
|
196
|
+
if (tables.length <= 1) {
|
|
197
|
+
return tables[0] ?? "";
|
|
198
|
+
}
|
|
199
|
+
const last = tables[tables.length - 1] ?? "";
|
|
200
|
+
return `${tables.slice(0, -1).join(", ")} and ${last}`;
|
|
201
|
+
};
|
|
202
|
+
const insightTitle = (t, insight) => ({
|
|
203
|
+
"high-error-rate": t("High error rate"),
|
|
204
|
+
"high-evictions": t("High cache eviction rate"),
|
|
205
|
+
"high-write-contention": t("High write contention"),
|
|
206
|
+
"low-cache-hit-rate": t("Low cache hit rate"),
|
|
207
|
+
"missing-index": t("Missing index"),
|
|
208
|
+
"slow-function": t("Slow function")
|
|
209
|
+
})[insight.kind];
|
|
210
|
+
const insightDetail = (t, insight) => ({
|
|
211
|
+
"high-error-rate": t("{rate} of calls failed.", {
|
|
212
|
+
rate: percent(insight.value)
|
|
213
|
+
}),
|
|
214
|
+
"high-evictions": t("{count} entries evicted recently.", {
|
|
215
|
+
count: insight.value
|
|
216
|
+
}),
|
|
217
|
+
"high-write-contention": t("{rate} of calls hit a write conflict — consider sharding to cut contention.", {
|
|
218
|
+
rate: percent(insight.value)
|
|
219
|
+
}),
|
|
220
|
+
"low-cache-hit-rate": t("{rate} hit rate over recent traffic.", {
|
|
221
|
+
rate: percent(insight.value)
|
|
222
|
+
}),
|
|
223
|
+
"missing-index": t("Slowest call took {duration} — it full-scanned {tables} with no index.", {
|
|
224
|
+
duration: seconds(insight.value),
|
|
225
|
+
tables: tableList(insight.tables ?? [])
|
|
226
|
+
}),
|
|
227
|
+
"slow-function": t("Slowest call took {duration}.", {
|
|
228
|
+
duration: seconds(insight.value)
|
|
229
|
+
})
|
|
230
|
+
})[insight.kind];
|
|
231
|
+
const AddIndexButton = (t0) => {
|
|
232
|
+
const $ = c(10);
|
|
233
|
+
const {
|
|
234
|
+
onJump,
|
|
235
|
+
table
|
|
236
|
+
} = t0;
|
|
237
|
+
const t = useT();
|
|
238
|
+
let t1;
|
|
239
|
+
if ($[0] !== onJump || $[1] !== table) {
|
|
240
|
+
t1 = () => {
|
|
241
|
+
onJump(table);
|
|
242
|
+
};
|
|
243
|
+
$[0] = onJump;
|
|
244
|
+
$[1] = table;
|
|
245
|
+
$[2] = t1;
|
|
246
|
+
} else {
|
|
247
|
+
t1 = $[2];
|
|
248
|
+
}
|
|
249
|
+
const onClick = t1;
|
|
250
|
+
const t2 = `in-add-index-${table}`;
|
|
251
|
+
let t3;
|
|
252
|
+
if ($[3] !== t || $[4] !== table) {
|
|
253
|
+
t3 = t("Add index on {table}", {
|
|
254
|
+
table
|
|
255
|
+
});
|
|
256
|
+
$[3] = t;
|
|
257
|
+
$[4] = table;
|
|
258
|
+
$[5] = t3;
|
|
259
|
+
} else {
|
|
260
|
+
t3 = $[5];
|
|
261
|
+
}
|
|
262
|
+
let t4;
|
|
263
|
+
if ($[6] !== onClick || $[7] !== t2 || $[8] !== t3) {
|
|
264
|
+
t4 = /* @__PURE__ */ jsxDEV(Button, {
|
|
265
|
+
"data-testid": t2,
|
|
266
|
+
onClick,
|
|
267
|
+
size: "sm",
|
|
268
|
+
type: "button",
|
|
269
|
+
variant: "outline",
|
|
270
|
+
children: t3
|
|
271
|
+
}, void 0, false);
|
|
272
|
+
$[6] = onClick;
|
|
273
|
+
$[7] = t2;
|
|
274
|
+
$[8] = t3;
|
|
275
|
+
$[9] = t4;
|
|
276
|
+
} else {
|
|
277
|
+
t4 = $[9];
|
|
278
|
+
}
|
|
279
|
+
return t4;
|
|
280
|
+
};
|
|
281
|
+
const InsightsPanel = ({
|
|
282
|
+
initialShardKey,
|
|
283
|
+
loadShardTraffic
|
|
284
|
+
}) => {
|
|
285
|
+
const client = useLunora();
|
|
286
|
+
const navigate = useNavigate();
|
|
287
|
+
const t = useT();
|
|
288
|
+
const [shardKey, setShardKey] = useState(initialShardKey ?? "");
|
|
289
|
+
const [metrics, setMetrics] = useState(null);
|
|
290
|
+
const [functions, setFunctions] = useState(null);
|
|
291
|
+
const [advisories, setAdvisories] = useState(null);
|
|
292
|
+
const [indexHits, setIndexHits] = useState(null);
|
|
293
|
+
const [declaredIndexes, setDeclaredIndexes] = useState(null);
|
|
294
|
+
const [shardTraffic, setShardTraffic] = useState(null);
|
|
295
|
+
const [error, setError] = useState(null);
|
|
296
|
+
const fanShardTraffic = loadShardTraffic ?? ((table) => client.shardTraffic(table));
|
|
297
|
+
const refresh = async (shard) => {
|
|
298
|
+
const [snapshot, stats, advisorySnapshot] = await Promise.allSettled([client.query(GET_METRICS, {}, callOptions(shard)), client.query(GET_FUNCTION_STATS, {}, callOptions(shard)), client.query(GET_ADVISORIES, {}, callOptions(shard))]);
|
|
299
|
+
if (snapshot.status === "rejected" && stats.status === "rejected") {
|
|
300
|
+
setError(errorMessage(snapshot.reason));
|
|
301
|
+
} else {
|
|
302
|
+
setError(null);
|
|
303
|
+
recordShard(shard);
|
|
304
|
+
}
|
|
305
|
+
setMetrics(snapshot.status === "fulfilled" ? snapshot.value : null);
|
|
306
|
+
setFunctions(stats.status === "fulfilled" ? stats.value.functions : null);
|
|
307
|
+
setAdvisories(advisorySnapshot.status === "fulfilled" ? advisorySnapshot.value.advisories : null);
|
|
308
|
+
setIndexHits(snapshot.status === "fulfilled" ? snapshot.value.indexHits ?? null : null);
|
|
309
|
+
let tableNames = [];
|
|
310
|
+
try {
|
|
311
|
+
const tables = await client.query(LIST_TABLES, {}, callOptions(shard));
|
|
312
|
+
tableNames = tables.map((table_0) => table_0.name);
|
|
313
|
+
const indexResults = await Promise.allSettled(tables.map(async (table_1) => [table_1.name, await client.query(LIST_TABLE_INDEXES, {
|
|
314
|
+
table: table_1.name
|
|
315
|
+
}, callOptions(shard))]));
|
|
316
|
+
const declared = [];
|
|
317
|
+
for (const result of indexResults) {
|
|
318
|
+
if (result.status === "fulfilled") {
|
|
319
|
+
const [name, payload] = result.value;
|
|
320
|
+
declared.push(...declaredIndexesFor(name, payload.indexes));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
setDeclaredIndexes(declared);
|
|
324
|
+
} catch {
|
|
325
|
+
setDeclaredIndexes(null);
|
|
326
|
+
}
|
|
327
|
+
try {
|
|
328
|
+
const traffic = await fanShardTraffic(tableNames[0] ?? "");
|
|
329
|
+
const byShard = /* @__PURE__ */ new Map();
|
|
330
|
+
for (const entry of traffic.shards) {
|
|
331
|
+
if (!byShard.has(entry.shardKey)) {
|
|
332
|
+
byShard.set(entry.shardKey, {
|
|
333
|
+
requests: entry.requests,
|
|
334
|
+
shardKey: entry.shardKey
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
setShardTraffic([...byShard.values()]);
|
|
339
|
+
} catch {
|
|
340
|
+
setShardTraffic(null);
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
useLiveShardSeed(shardKey, refresh);
|
|
344
|
+
useEffect(() => {
|
|
345
|
+
const onVisible = () => {
|
|
346
|
+
if (document.visibilityState === "visible") {
|
|
347
|
+
fireAndForget(refresh(shardKey));
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
351
|
+
return () => {
|
|
352
|
+
document.removeEventListener("visibilitychange", onVisible);
|
|
353
|
+
};
|
|
354
|
+
}, [refresh, shardKey]);
|
|
355
|
+
const jumpToSchemaIndex = (table_2) => {
|
|
356
|
+
fireAndForget(navigate({
|
|
357
|
+
search: {
|
|
358
|
+
table: table_2
|
|
359
|
+
},
|
|
360
|
+
to: "/schema"
|
|
361
|
+
}));
|
|
362
|
+
};
|
|
363
|
+
const insights = deriveInsights(metrics, functions);
|
|
364
|
+
const missingIndexTables = new Set(insights.filter((insight) => insight.kind === "missing-index").flatMap((insight_0) => insight_0.tables ?? []));
|
|
365
|
+
const runtimeRows = deriveRuntimeAdvisories({
|
|
366
|
+
declaredIndexes: declaredIndexes ?? [],
|
|
367
|
+
functions,
|
|
368
|
+
indexHits,
|
|
369
|
+
shardTraffic,
|
|
370
|
+
suppressHotScanTables: missingIndexTables
|
|
371
|
+
});
|
|
372
|
+
const rows = useMemo(() => {
|
|
373
|
+
const insightRows = insights.map((insight_1) => {
|
|
374
|
+
const tables_0 = insight_1.kind === "missing-index" ? insight_1.tables ?? [] : [];
|
|
375
|
+
return {
|
|
376
|
+
action: tables_0.length > 0 ? tables_0.map((table_3) => /* @__PURE__ */ jsxDEV(AddIndexButton, {
|
|
377
|
+
onJump: jumpToSchemaIndex,
|
|
378
|
+
table: table_3
|
|
379
|
+
}, table_3, false)) : void 0,
|
|
380
|
+
description: insight_1.message === void 0 ? insightDetail(t, insight_1) : `${insightDetail(t, insight_1)} — ${insight_1.message}`,
|
|
381
|
+
entity: insight_1.fn,
|
|
382
|
+
issueType: insightTitle(t, insight_1),
|
|
383
|
+
key: `${insight_1.kind}:${insight_1.fn ?? ""}`,
|
|
384
|
+
level: insight_1.severity
|
|
385
|
+
};
|
|
386
|
+
});
|
|
387
|
+
const indexAdvisoryLints = /* @__PURE__ */ new Set(["unindexed_foreign_key", "unindexed_relation_target"]);
|
|
388
|
+
const staticRows = (advisories ?? []).map((finding) => {
|
|
389
|
+
const base = advisoryRow(finding);
|
|
390
|
+
if (indexAdvisoryLints.has(finding.name) && hasIndexMetadata(finding.metadata)) {
|
|
391
|
+
const {
|
|
392
|
+
table: table_4,
|
|
393
|
+
suggestedIndex
|
|
394
|
+
} = finding.metadata;
|
|
395
|
+
const testId = `in-apply-index-${table_4}-${suggestedIndex.name}`;
|
|
396
|
+
return {
|
|
397
|
+
...base,
|
|
398
|
+
action: /* @__PURE__ */ jsxDEV(ApplyIndexButton, {
|
|
399
|
+
fields: suggestedIndex.fields,
|
|
400
|
+
indexName: suggestedIndex.name,
|
|
401
|
+
table: table_4,
|
|
402
|
+
testId
|
|
403
|
+
}, void 0, false)
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
return base;
|
|
407
|
+
});
|
|
408
|
+
return [...staticRows, ...runtimeRows, ...insightRows];
|
|
409
|
+
}, [advisories, insights, jumpToSchemaIndex, runtimeRows, t]);
|
|
410
|
+
const toolbar = /* @__PURE__ */ jsxDEV(ShardInput, {
|
|
411
|
+
onChange: setShardKey,
|
|
412
|
+
testId: "in-shard-input",
|
|
413
|
+
value: shardKey
|
|
414
|
+
}, void 0, false);
|
|
415
|
+
return /* @__PURE__ */ jsxDEV(AdvisorView, {
|
|
416
|
+
error,
|
|
417
|
+
rows,
|
|
418
|
+
testId: "lunora-insights",
|
|
419
|
+
toolbar
|
|
420
|
+
}, void 0, false);
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
export { InsightsPanel };
|