@lunora/studio 0.0.0 → 1.0.0-alpha.10

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.
Files changed (80) hide show
  1. package/LICENSE.md +105 -0
  2. package/README.md +123 -9
  3. package/__assets__/package-og.svg +14 -0
  4. package/dist/index.d.ts +1403 -0
  5. package/dist/index.js +41 -0
  6. package/dist/mount.d.ts +21 -0
  7. package/dist/mount.js +26 -0
  8. package/dist/packem_shared/ADMIN_FUNCTIONS-DjXgvJb6.js +46 -0
  9. package/dist/packem_shared/ApiDocsPanel-yVSGHK3H.js +842 -0
  10. package/dist/packem_shared/ApiReferencePanel-1vubqG5s.js +229 -0
  11. package/dist/packem_shared/ApiTab-rwttyQ1-.js +251 -0
  12. package/dist/packem_shared/AuditPanel-D9Eh7j7E.js +200 -0
  13. package/dist/packem_shared/CommandPalette-BtgkBzZI.js +373 -0
  14. package/dist/packem_shared/ConfirmButton-C2eVi2Qv.js +59 -0
  15. package/dist/packem_shared/ConnectionBadge-B2DDM2Tn.js +111 -0
  16. package/dist/packem_shared/DEFAULT_AUTO_REFRESH_MS-Vxwaxx51.js +50 -0
  17. package/dist/packem_shared/DEFAULT_INSIGHT_THRESHOLDS-DjF0h-gA.js +89 -0
  18. package/dist/packem_shared/DEFAULT_LOCALE-CiX4W7Kr.js +27 -0
  19. package/dist/packem_shared/DataBrowser-CyckZRwj.js +4480 -0
  20. package/dist/packem_shared/DataFilters-FNquMaiu.js +249 -0
  21. package/dist/packem_shared/ErrorBoundary-BzAApI7J.js +66 -0
  22. package/dist/packem_shared/ExportImportPanel-BGJqFK9X.js +197 -0
  23. package/dist/packem_shared/FileBrowser-0pCpdKPE.js +2932 -0
  24. package/dist/packem_shared/FunctionRunner-7NH3KcTg.js +343 -0
  25. package/dist/packem_shared/FunctionStatsPanel-CXynSuqT.js +375 -0
  26. package/dist/packem_shared/GlobalDataBrowser-DAxzB-Br.js +289 -0
  27. package/dist/packem_shared/HealthPanel-CyLs7qZm.js +648 -0
  28. package/dist/packem_shared/HomePanel-BOkFRbuj.js +1334 -0
  29. package/dist/packem_shared/InsightsPanel-CvXMoZuv.js +461 -0
  30. package/dist/packem_shared/LogsPanel-BrUstOk6.js +827 -0
  31. package/dist/packem_shared/MailPanel-GnnrNqL6.js +440 -0
  32. package/dist/packem_shared/MetricsPanel-BN_uUU-_.js +1611 -0
  33. package/dist/packem_shared/MigrationsPanel-DaZuUHje.js +239 -0
  34. package/dist/packem_shared/OpenRpcReferencePanel-vbcYbv3V.js +191 -0
  35. package/dist/packem_shared/PitrPanel-LJGNP_IX.js +252 -0
  36. package/dist/packem_shared/STUDIO_ROOT_CLASS-D12gX2dV.js +3 -0
  37. package/dist/packem_shared/ScheduledJobs-UmVaMbJy.js +262 -0
  38. package/dist/packem_shared/SchemaViewer-Bam6ylvw.js +2512 -0
  39. package/dist/packem_shared/SecurityAdvisorPanel-DTmjUNqt.js +92 -0
  40. package/dist/packem_shared/SettingsPanel-C2wDK-8F.js +390 -0
  41. package/dist/packem_shared/ShardInput-gx_DZXXn.js +107 -0
  42. package/dist/packem_shared/SqlEditorPanel-D5wMIxcg.js +13 -0
  43. package/dist/packem_shared/Studio-DpgYe4qt.js +33 -0
  44. package/dist/packem_shared/StudioApp-C7aaizqc.js +657 -0
  45. package/dist/packem_shared/StudioI18nProvider-B-tDCZIi.js +48 -0
  46. package/dist/packem_shared/TableEditor-DzK3Z6rL.js +371 -0
  47. package/dist/packem_shared/advisor-view-CGcfMiPq.js +159 -0
  48. package/dist/packem_shared/aggregateMetrics-D4nUHEKU.js +108 -0
  49. package/dist/packem_shared/app.d-Nbhpihxv.d.ts +300 -0
  50. package/dist/packem_shared/badge-B2PKA1-5.js +49 -0
  51. package/dist/packem_shared/bar-chart-CzJAgqkp.js +3245 -0
  52. package/dist/packem_shared/button-BhsN2uZH.js +49 -0
  53. package/dist/packem_shared/card-DURq3ElK.js +175 -0
  54. package/dist/packem_shared/cf-links-2cktDedA.js +8 -0
  55. package/dist/packem_shared/checkbox-UNkzAxl-.js +63 -0
  56. package/dist/packem_shared/data-grid-B5CaV3wS.js +183 -0
  57. package/dist/packem_shared/dropdown-menu-WY4B_eJO.js +280 -0
  58. package/dist/packem_shared/empty-state-DY_oe0k6.js +98 -0
  59. package/dist/packem_shared/grid-features-CzUfqgsM.js +840 -0
  60. package/dist/packem_shared/input-XH4r1Pt1.js +53 -0
  61. package/dist/packem_shared/internal-rXJEAhXS.js +68 -0
  62. package/dist/packem_shared/label-D8ykjn5J.js +46 -0
  63. package/dist/packem_shared/live-status-FZ0psC1z.js +44 -0
  64. package/dist/packem_shared/reference-view-B4E49Lip.js +2180 -0
  65. package/dist/packem_shared/shard-history-DyebH1R5.js +38 -0
  66. package/dist/packem_shared/sparkline-10dG-_f0.js +93 -0
  67. package/dist/packem_shared/sql-editor-panel-Smfl_FDF.js +2562 -0
  68. package/dist/packem_shared/storage-tier-CL98eOvn.js +85 -0
  69. package/dist/packem_shared/studio-9Mq0PdMi.js +10831 -0
  70. package/dist/packem_shared/table-_RzNvy3R.js +246 -0
  71. package/dist/packem_shared/table-list-sidebar-Bg7uzbvC.js +832 -0
  72. package/dist/packem_shared/textarea-D3gaCU_-.js +46 -0
  73. package/dist/packem_shared/use-admin-query-4JQW2rr1.js +88 -0
  74. package/dist/packem_shared/useDebounced-Dxncpg6z.js +32 -0
  75. package/dist/packem_shared/utils-B05Dmz_H.js +8 -0
  76. package/dist/packem_shared/virtual-rect-CVMUskSm.js +10 -0
  77. package/dist/standalone/studio.js +378 -0
  78. package/dist/styles.css +2 -0
  79. package/package.json +78 -17
  80. package/src/theme.css +59 -0
@@ -0,0 +1,461 @@
1
+ import { c } from 'react/compiler-runtime';
2
+ import { useLunora } from '@lunora/react';
3
+ import { useNavigate } from '@tanstack/react-router';
4
+ import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
5
+ import { ShardInput } from './ShardInput-gx_DZXXn.js';
6
+ import { B as Button } from './button-BhsN2uZH.js';
7
+ import { u as useAdminQuery } from './use-admin-query-4JQW2rr1.js';
8
+ import useDebounced from './useDebounced-Dxncpg6z.js';
9
+ import { useT } from './DEFAULT_LOCALE-CiX4W7Kr.js';
10
+ import { ADMIN_FUNCTIONS } from './ADMIN_FUNCTIONS-DjXgvJb6.js';
11
+ import { f as fireAndForget, c as callOptions, a as adminRef } from './internal-rXJEAhXS.js';
12
+ import { r as recordShard } from './shard-history-DyebH1R5.js';
13
+ import { a as advisoryRow, A as AdvisorView } from './advisor-view-CGcfMiPq.js';
14
+ import { ConfirmButton } from './ConfirmButton-C2eVi2Qv.js';
15
+ import { jsxDEV } from 'react/jsx-dev-runtime';
16
+ import { deriveInsights } from './DEFAULT_INSIGHT_THRESHOLDS-DjF0h-gA.js';
17
+ import { runAdvisor, RUNTIME_LINTS } from '@lunora/advisor';
18
+
19
+ const hasIndexMetadata = (metadata) => {
20
+ const {
21
+ suggestedIndex,
22
+ table
23
+ } = metadata;
24
+ if (typeof table !== "string" || table.length === 0) {
25
+ return false;
26
+ }
27
+ if (typeof suggestedIndex !== "object" || suggestedIndex === null || !Array.isArray(suggestedIndex["fields"]) || suggestedIndex["fields"].length === 0) {
28
+ return false;
29
+ }
30
+ return true;
31
+ };
32
+ const composeCreateIndex = (table, indexName, fields) => {
33
+ const quotedTable = `"${table}"`;
34
+ const quotedIndex = `"${indexName}"`;
35
+ const quotedColumns = fields.map((f) => `"${f}"`).join(", ");
36
+ return `CREATE INDEX IF NOT EXISTS ${quotedIndex} ON ${quotedTable} (${quotedColumns});`;
37
+ };
38
+
39
+ const ApplyIndexButton = (t0) => {
40
+ const $ = c(19);
41
+ const {
42
+ fields,
43
+ indexName,
44
+ table,
45
+ testId
46
+ } = t0;
47
+ const t = useT();
48
+ const [applied, setApplied] = useState(false);
49
+ let t1;
50
+ if ($[0] !== fields || $[1] !== indexName || $[2] !== table) {
51
+ t1 = () => {
52
+ const clipboard = "navigator" in globalThis ? globalThis.navigator.clipboard : void 0;
53
+ if (clipboard === void 0) {
54
+ return;
55
+ }
56
+ const sql = composeCreateIndex(table, indexName, fields);
57
+ fireAndForget(clipboard.writeText(sql));
58
+ setApplied(true);
59
+ };
60
+ $[0] = fields;
61
+ $[1] = indexName;
62
+ $[2] = table;
63
+ $[3] = t1;
64
+ } else {
65
+ t1 = $[3];
66
+ }
67
+ const onConfirm = t1;
68
+ if (applied) {
69
+ const t22 = `${testId}-applied`;
70
+ let t32;
71
+ if ($[4] !== t) {
72
+ t32 = t("CREATE INDEX SQL copied to clipboard.");
73
+ $[4] = t;
74
+ $[5] = t32;
75
+ } else {
76
+ t32 = $[5];
77
+ }
78
+ let t42;
79
+ if ($[6] !== t22 || $[7] !== t32) {
80
+ t42 = /* @__PURE__ */ jsxDEV("span", {
81
+ className: "text-xs text-muted-foreground",
82
+ "data-testid": t22,
83
+ children: t32
84
+ }, void 0, false);
85
+ $[6] = t22;
86
+ $[7] = t32;
87
+ $[8] = t42;
88
+ } else {
89
+ t42 = $[8];
90
+ }
91
+ return t42;
92
+ }
93
+ let t2;
94
+ if ($[9] !== t) {
95
+ t2 = t("Apply?");
96
+ $[9] = t;
97
+ $[10] = t2;
98
+ } else {
99
+ t2 = $[10];
100
+ }
101
+ let t3;
102
+ if ($[11] !== t || $[12] !== table) {
103
+ t3 = t("Apply index on {table}", {
104
+ table
105
+ });
106
+ $[11] = t;
107
+ $[12] = table;
108
+ $[13] = t3;
109
+ } else {
110
+ t3 = $[13];
111
+ }
112
+ let t4;
113
+ if ($[14] !== onConfirm || $[15] !== t2 || $[16] !== t3 || $[17] !== testId) {
114
+ t4 = /* @__PURE__ */ jsxDEV(ConfirmButton, {
115
+ confirmLabel: t2,
116
+ onConfirm,
117
+ testId,
118
+ children: t3
119
+ }, void 0, false);
120
+ $[14] = onConfirm;
121
+ $[15] = t2;
122
+ $[16] = t3;
123
+ $[17] = testId;
124
+ $[18] = t4;
125
+ } else {
126
+ t4 = $[18];
127
+ }
128
+ return t4;
129
+ };
130
+
131
+ const reconcileIndexHits = (declaredIndexes, indexHits) => {
132
+ const readsByKey = /* @__PURE__ */ new Map();
133
+ for (const hit of indexHits) {
134
+ const key = `${hit.table} ${hit.index}`;
135
+ readsByKey.set(key, (readsByKey.get(key) ?? 0) + hit.reads);
136
+ }
137
+ return declaredIndexes.map((declared) => {
138
+ return {
139
+ index: declared.index,
140
+ reads: readsByKey.get(`${declared.table} ${declared.index}`) ?? 0,
141
+ table: declared.table
142
+ };
143
+ });
144
+ };
145
+ const aggregateTableScans = (functions) => {
146
+ const scansByTable = /* @__PURE__ */ new Map();
147
+ for (const callStat of functions) {
148
+ for (const attribution of callStat.scannedTables ?? []) {
149
+ scansByTable.set(attribution.table, (scansByTable.get(attribution.table) ?? 0) + attribution.scans);
150
+ }
151
+ }
152
+ return [...scansByTable].map(([table, scans]) => {
153
+ return {
154
+ scans,
155
+ table
156
+ };
157
+ });
158
+ };
159
+ const declaredIndexesFor = (table, indexes) => indexes.map((index) => {
160
+ return {
161
+ index: index.name,
162
+ table
163
+ };
164
+ });
165
+ const deriveRuntimeAdvisories = (inputs) => {
166
+ const inDoIndexHits = reconcileIndexHits(inputs.declaredIndexes ?? [], inputs.indexHits ?? []);
167
+ const inDoTableScans = aggregateTableScans(inputs.functions ?? []);
168
+ const inDoShardTraffic = inputs.shardTraffic ?? [];
169
+ const analytics = inputs.analyticsMetrics;
170
+ const indexHits = analytics && analytics.indexHits.length > 0 ? analytics.indexHits : inDoIndexHits;
171
+ const tableScans = analytics && analytics.tableScans.length > 0 ? analytics.tableScans : inDoTableScans;
172
+ const shardTraffic = analytics && analytics.shardTraffic.length > 0 ? analytics.shardTraffic : inDoShardTraffic;
173
+ const findings = runAdvisor({
174
+ indexHits,
175
+ schema: {
176
+ tables: []
177
+ },
178
+ shardTraffic,
179
+ tableScans
180
+ }, {
181
+ lints: RUNTIME_LINTS,
182
+ source: "runtime"
183
+ });
184
+ const suppress = inputs.suppressHotScanTables;
185
+ const visible = suppress === void 0 ? findings : findings.filter((finding) => !(finding.metadata["kind"] === "hot_scan" && typeof finding.metadata["table"] === "string" && suppress.has(finding.metadata["table"])));
186
+ return visible.map((finding) => advisoryRow(finding));
187
+ };
188
+
189
+ const LIST_TABLES = adminRef(ADMIN_FUNCTIONS.listTables);
190
+ const LIST_TABLE_INDEXES = adminRef(ADMIN_FUNCTIONS.listTableIndexes);
191
+ const percent = (rate) => `${(rate * 100).toFixed(1)}%`;
192
+ const seconds = (ms) => `${(ms / 1e3).toFixed(2)}s`;
193
+ const tableList = (tables) => {
194
+ if (tables.length <= 1) {
195
+ return tables[0] ?? "";
196
+ }
197
+ const last = tables[tables.length - 1] ?? "";
198
+ return `${tables.slice(0, -1).join(", ")} and ${last}`;
199
+ };
200
+ const insightTitle = (t, insight) => ({
201
+ "high-error-rate": t("High error rate"),
202
+ "high-evictions": t("High cache eviction rate"),
203
+ "high-write-contention": t("High write contention"),
204
+ "low-cache-hit-rate": t("Low cache hit rate"),
205
+ "missing-index": t("Missing index"),
206
+ "slow-function": t("Slow function")
207
+ })[insight.kind];
208
+ const insightDetail = (t, insight) => ({
209
+ "high-error-rate": t("{rate} of calls failed.", {
210
+ rate: percent(insight.value)
211
+ }),
212
+ "high-evictions": t("{count} entries evicted recently.", {
213
+ count: insight.value
214
+ }),
215
+ "high-write-contention": t("{rate} of calls hit a write conflict — consider sharding to cut contention.", {
216
+ rate: percent(insight.value)
217
+ }),
218
+ "low-cache-hit-rate": t("{rate} hit rate over recent traffic.", {
219
+ rate: percent(insight.value)
220
+ }),
221
+ "missing-index": t("Slowest call took {duration} — it full-scanned {tables} with no index.", {
222
+ duration: seconds(insight.value),
223
+ tables: tableList(insight.tables ?? [])
224
+ }),
225
+ "slow-function": t("Slowest call took {duration}.", {
226
+ duration: seconds(insight.value)
227
+ })
228
+ })[insight.kind];
229
+ const AddIndexButton = (t0) => {
230
+ const $ = c(10);
231
+ const {
232
+ onJump,
233
+ table
234
+ } = t0;
235
+ const t = useT();
236
+ let t1;
237
+ if ($[0] !== onJump || $[1] !== table) {
238
+ t1 = () => {
239
+ onJump(table);
240
+ };
241
+ $[0] = onJump;
242
+ $[1] = table;
243
+ $[2] = t1;
244
+ } else {
245
+ t1 = $[2];
246
+ }
247
+ const onClick = t1;
248
+ const t2 = `in-add-index-${table}`;
249
+ let t3;
250
+ if ($[3] !== t || $[4] !== table) {
251
+ t3 = t("Add index on {table}", {
252
+ table
253
+ });
254
+ $[3] = t;
255
+ $[4] = table;
256
+ $[5] = t3;
257
+ } else {
258
+ t3 = $[5];
259
+ }
260
+ let t4;
261
+ if ($[6] !== onClick || $[7] !== t2 || $[8] !== t3) {
262
+ t4 = /* @__PURE__ */ jsxDEV(Button, {
263
+ "data-testid": t2,
264
+ onClick,
265
+ size: "sm",
266
+ type: "button",
267
+ variant: "outline",
268
+ children: t3
269
+ }, void 0, false);
270
+ $[6] = onClick;
271
+ $[7] = t2;
272
+ $[8] = t3;
273
+ $[9] = t4;
274
+ } else {
275
+ t4 = $[9];
276
+ }
277
+ return t4;
278
+ };
279
+ const InsightsPanel = ({
280
+ initialShardKey,
281
+ loadShardTraffic
282
+ }) => {
283
+ const client = useLunora();
284
+ const navigate = useNavigate();
285
+ const t = useT();
286
+ const [shardKey, setShardKey] = useState(initialShardKey ?? "");
287
+ const [declaredIndexes, setDeclaredIndexes] = useState(null);
288
+ const [shardTraffic, setShardTraffic] = useState(null);
289
+ const debouncedShard = useDebounced(shardKey.trim(), 400);
290
+ const latestEnumeratedShard = useRef("");
291
+ const metricsQuery = useAdminQuery(ADMIN_FUNCTIONS.getMetrics, {}, {
292
+ live: true,
293
+ shardKey: debouncedShard
294
+ });
295
+ const functionsQuery = useAdminQuery(ADMIN_FUNCTIONS.getFunctionStats, {}, {
296
+ live: true,
297
+ shardKey: debouncedShard
298
+ });
299
+ const advisoriesQuery = useAdminQuery(ADMIN_FUNCTIONS.getAdvisories, {}, {
300
+ live: true,
301
+ shardKey: debouncedShard
302
+ });
303
+ const metrics = metricsQuery.data ?? null;
304
+ const functions = functionsQuery.data?.functions ?? null;
305
+ const advisories = advisoriesQuery.data?.advisories ?? null;
306
+ const indexHits = metricsQuery.data?.indexHits ?? null;
307
+ const error = metricsQuery.error !== null && functionsQuery.error !== null ? metricsQuery.error : null;
308
+ const fanShardTraffic = loadShardTraffic ?? ((table) => client.shardTraffic(table));
309
+ const loadShardTrafficFeed = async (tableNames, shard) => {
310
+ const seedTable = tableNames[0];
311
+ if (seedTable === void 0 || seedTable === "") {
312
+ if (latestEnumeratedShard.current === shard) {
313
+ setShardTraffic(null);
314
+ }
315
+ return;
316
+ }
317
+ try {
318
+ const traffic = await fanShardTraffic(seedTable);
319
+ if (latestEnumeratedShard.current !== shard) {
320
+ return;
321
+ }
322
+ const byShard = /* @__PURE__ */ new Map();
323
+ for (const entry of traffic.shards) {
324
+ if (!byShard.has(entry.shardKey)) {
325
+ byShard.set(entry.shardKey, {
326
+ requests: entry.requests,
327
+ shardKey: entry.shardKey
328
+ });
329
+ }
330
+ }
331
+ setShardTraffic([...byShard.values()]);
332
+ } catch {
333
+ if (latestEnumeratedShard.current === shard) {
334
+ setShardTraffic(null);
335
+ }
336
+ }
337
+ };
338
+ const enumerateShard = useCallback(
339
+ async (shard) => {
340
+ latestEnumeratedShard.current = shard;
341
+ let tableNames = [];
342
+ try {
343
+ const tables = await client.query(LIST_TABLES, {}, callOptions(shard));
344
+ if (latestEnumeratedShard.current !== shard) {
345
+ return;
346
+ }
347
+ recordShard(shard);
348
+ tableNames = tables.map((table) => table.name);
349
+ const indexResults = await Promise.allSettled(tables.map(async (table) => [table.name, await client.query(LIST_TABLE_INDEXES, {
350
+ table: table.name
351
+ }, callOptions(shard))]));
352
+ if (latestEnumeratedShard.current !== shard) {
353
+ return;
354
+ }
355
+ const declared = [];
356
+ for (const result of indexResults) {
357
+ if (result.status === "fulfilled") {
358
+ const [name, payload] = result.value;
359
+ declared.push(...declaredIndexesFor(name, payload.indexes));
360
+ }
361
+ }
362
+ setDeclaredIndexes(declared);
363
+ } catch {
364
+ if (latestEnumeratedShard.current === shard) {
365
+ setDeclaredIndexes(null);
366
+ }
367
+ }
368
+ await loadShardTrafficFeed(tableNames, shard);
369
+ },
370
+ // `fanShardTraffic`/`loadShardTrafficFeed` are render-fresh closures; the
371
+ // enumeration only needs to re-run on a shard change (or a manual visibility
372
+ // refresh), so they're intentionally excluded from the cache key.
373
+ // eslint-disable-next-line react-hooks/exhaustive-deps
374
+ [client]
375
+ );
376
+ useEffect(() => {
377
+ fireAndForget(enumerateShard(debouncedShard));
378
+ }, [debouncedShard, enumerateShard]);
379
+ useEffect(() => {
380
+ const onVisible = () => {
381
+ if (document.visibilityState === "visible") {
382
+ metricsQuery.refetch();
383
+ functionsQuery.refetch();
384
+ advisoriesQuery.refetch();
385
+ fireAndForget(enumerateShard(debouncedShard));
386
+ }
387
+ };
388
+ document.addEventListener("visibilitychange", onVisible);
389
+ return () => {
390
+ document.removeEventListener("visibilitychange", onVisible);
391
+ };
392
+ }, [advisoriesQuery, debouncedShard, enumerateShard, functionsQuery, metricsQuery]);
393
+ const jumpToSchemaIndex = useCallback((table) => {
394
+ fireAndForget(navigate({
395
+ search: {
396
+ table
397
+ },
398
+ to: "/schema"
399
+ }));
400
+ }, [navigate]);
401
+ const insights = deriveInsights(metrics, functions);
402
+ const missingIndexTables = new Set(insights.filter((insight) => insight.kind === "missing-index").flatMap((insight) => insight.tables ?? []));
403
+ const runtimeRows = deriveRuntimeAdvisories({
404
+ declaredIndexes: declaredIndexes ?? [],
405
+ functions,
406
+ indexHits,
407
+ shardTraffic,
408
+ suppressHotScanTables: missingIndexTables
409
+ });
410
+ const rows = useMemo(() => {
411
+ const insightRows = insights.map((insight) => {
412
+ const tables = insight.kind === "missing-index" ? insight.tables ?? [] : [];
413
+ return {
414
+ action: tables.length > 0 ? tables.map((table) => /* @__PURE__ */ jsxDEV(AddIndexButton, {
415
+ onJump: jumpToSchemaIndex,
416
+ table
417
+ }, table, false)) : void 0,
418
+ description: insight.message === void 0 ? insightDetail(t, insight) : `${insightDetail(t, insight)} — ${insight.message}`,
419
+ entity: insight.fn,
420
+ issueType: insightTitle(t, insight),
421
+ key: `${insight.kind}:${insight.fn ?? ""}`,
422
+ level: insight.severity
423
+ };
424
+ });
425
+ const indexAdvisoryLints = /* @__PURE__ */ new Set(["unindexed_foreign_key", "unindexed_relation_target"]);
426
+ const staticRows = (advisories ?? []).map((finding) => {
427
+ const base = advisoryRow(finding);
428
+ if (indexAdvisoryLints.has(finding.name) && hasIndexMetadata(finding.metadata)) {
429
+ const {
430
+ table,
431
+ suggestedIndex
432
+ } = finding.metadata;
433
+ const testId = `in-apply-index-${table}-${suggestedIndex.name}`;
434
+ return {
435
+ ...base,
436
+ action: /* @__PURE__ */ jsxDEV(ApplyIndexButton, {
437
+ fields: suggestedIndex.fields,
438
+ indexName: suggestedIndex.name,
439
+ table,
440
+ testId
441
+ }, void 0, false)
442
+ };
443
+ }
444
+ return base;
445
+ });
446
+ return [...staticRows, ...runtimeRows, ...insightRows];
447
+ }, [advisories, insights, jumpToSchemaIndex, runtimeRows, t]);
448
+ const toolbar = /* @__PURE__ */ jsxDEV(ShardInput, {
449
+ onChange: setShardKey,
450
+ testId: "in-shard-input",
451
+ value: shardKey
452
+ }, void 0, false);
453
+ return /* @__PURE__ */ jsxDEV(AdvisorView, {
454
+ error,
455
+ rows,
456
+ testId: "lunora-insights",
457
+ toolbar
458
+ }, void 0, false);
459
+ };
460
+
461
+ export { InsightsPanel };