@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.
- package/LICENSE.md +105 -0
- package/README.md +123 -9
- package/__assets__/package-og.svg +14 -0
- package/dist/index.d.ts +1403 -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_FUNCTIONS-DjXgvJb6.js +46 -0
- package/dist/packem_shared/ApiDocsPanel-yVSGHK3H.js +842 -0
- package/dist/packem_shared/ApiReferencePanel-1vubqG5s.js +229 -0
- package/dist/packem_shared/ApiTab-rwttyQ1-.js +251 -0
- package/dist/packem_shared/AuditPanel-D9Eh7j7E.js +200 -0
- package/dist/packem_shared/CommandPalette-BtgkBzZI.js +373 -0
- package/dist/packem_shared/ConfirmButton-C2eVi2Qv.js +59 -0
- package/dist/packem_shared/ConnectionBadge-B2DDM2Tn.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/DEFAULT_LOCALE-CiX4W7Kr.js +27 -0
- package/dist/packem_shared/DataBrowser-CyckZRwj.js +4480 -0
- package/dist/packem_shared/DataFilters-FNquMaiu.js +249 -0
- package/dist/packem_shared/ErrorBoundary-BzAApI7J.js +66 -0
- package/dist/packem_shared/ExportImportPanel-BGJqFK9X.js +197 -0
- package/dist/packem_shared/FileBrowser-0pCpdKPE.js +2932 -0
- package/dist/packem_shared/FunctionRunner-7NH3KcTg.js +343 -0
- package/dist/packem_shared/FunctionStatsPanel-CXynSuqT.js +375 -0
- package/dist/packem_shared/GlobalDataBrowser-DAxzB-Br.js +289 -0
- package/dist/packem_shared/HealthPanel-CyLs7qZm.js +648 -0
- package/dist/packem_shared/HomePanel-BOkFRbuj.js +1334 -0
- package/dist/packem_shared/InsightsPanel-CvXMoZuv.js +461 -0
- package/dist/packem_shared/LogsPanel-BrUstOk6.js +827 -0
- package/dist/packem_shared/MailPanel-GnnrNqL6.js +440 -0
- package/dist/packem_shared/MetricsPanel-BN_uUU-_.js +1611 -0
- package/dist/packem_shared/MigrationsPanel-DaZuUHje.js +239 -0
- package/dist/packem_shared/OpenRpcReferencePanel-vbcYbv3V.js +191 -0
- package/dist/packem_shared/PitrPanel-LJGNP_IX.js +252 -0
- package/dist/packem_shared/STUDIO_ROOT_CLASS-D12gX2dV.js +3 -0
- package/dist/packem_shared/ScheduledJobs-UmVaMbJy.js +262 -0
- package/dist/packem_shared/SchemaViewer-Bam6ylvw.js +2512 -0
- package/dist/packem_shared/SecurityAdvisorPanel-DTmjUNqt.js +92 -0
- package/dist/packem_shared/SettingsPanel-C2wDK-8F.js +390 -0
- package/dist/packem_shared/ShardInput-gx_DZXXn.js +107 -0
- package/dist/packem_shared/SqlEditorPanel-D5wMIxcg.js +13 -0
- package/dist/packem_shared/Studio-DpgYe4qt.js +33 -0
- package/dist/packem_shared/StudioApp-C7aaizqc.js +657 -0
- package/dist/packem_shared/StudioI18nProvider-B-tDCZIi.js +48 -0
- package/dist/packem_shared/TableEditor-DzK3Z6rL.js +371 -0
- package/dist/packem_shared/advisor-view-CGcfMiPq.js +159 -0
- package/dist/packem_shared/aggregateMetrics-D4nUHEKU.js +108 -0
- package/dist/packem_shared/app.d-Nbhpihxv.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-2cktDedA.js +8 -0
- package/dist/packem_shared/checkbox-UNkzAxl-.js +63 -0
- package/dist/packem_shared/data-grid-B5CaV3wS.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-CzUfqgsM.js +840 -0
- package/dist/packem_shared/input-XH4r1Pt1.js +53 -0
- package/dist/packem_shared/internal-rXJEAhXS.js +68 -0
- package/dist/packem_shared/label-D8ykjn5J.js +46 -0
- package/dist/packem_shared/live-status-FZ0psC1z.js +44 -0
- package/dist/packem_shared/reference-view-B4E49Lip.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-Smfl_FDF.js +2562 -0
- package/dist/packem_shared/storage-tier-CL98eOvn.js +85 -0
- package/dist/packem_shared/studio-9Mq0PdMi.js +10831 -0
- package/dist/packem_shared/table-_RzNvy3R.js +246 -0
- package/dist/packem_shared/table-list-sidebar-Bg7uzbvC.js +832 -0
- package/dist/packem_shared/textarea-D3gaCU_-.js +46 -0
- package/dist/packem_shared/use-admin-query-4JQW2rr1.js +88 -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 +378 -0
- package/dist/styles.css +2 -0
- package/package.json +78 -17
- 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 };
|