v0.1.0-pre.050
This commit is contained in:
60
kb-app-demo-desktop/frontend/ts/demo_sql_diag.ts
Normal file
60
kb-app-demo-desktop/frontend/ts/demo_sql_diag.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
// file: kb-app-demo-desktop/frontend/ts/demo_sql_diag.ts
|
||||
// version: 2
|
||||
|
||||
import * as bootstrap from "bootstrap";
|
||||
import "simplebar";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log";
|
||||
import { jsonText, renderJsonViewer, renderTables, setText } from "./demo_sql_tables";
|
||||
import type { DemoSqlDiagPayload } from "./bindings/kb_app_demo_desktop/demo_sql_diag/DemoSqlDiagPayload";
|
||||
|
||||
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
|
||||
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
|
||||
|
||||
let latestPayload: DemoSqlDiagPayload | null = null;
|
||||
|
||||
async function refreshDiagnostics(): Promise<void> {
|
||||
setText("#sqlDiagStatus", "Chargement...");
|
||||
try {
|
||||
const payload = await invoke<DemoSqlDiagPayload>("load_demo_sql_diag");
|
||||
setText("#sqlDiagConfigPath", payload.configPath);
|
||||
setText("#sqlDiagProfile", payload.activeProfileName);
|
||||
setText("#sqlDiagBackend", payload.backend);
|
||||
setText("#sqlDiagDsn", payload.maskedDsn);
|
||||
setText("#sqlDiagSchema", payload.currentSchema ?? "—");
|
||||
setText("#sqlDiagHealth", payload.healthStatus);
|
||||
setText("#sqlDiagMigration", payload.migrationStatus);
|
||||
setText("#sqlDiagStatus", "OK");
|
||||
renderTables("#sqlDiagTableBody", payload.tables);
|
||||
latestPayload = payload;
|
||||
renderJsonViewer("#sqlDiagJson", payload);
|
||||
frontendDebug("kb_app_demo.frontend.demo_sql_diag", "SQL diagnostics refreshed");
|
||||
} catch (caughtError) {
|
||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||
setText("#sqlDiagStatus", `Erreur : ${message}`);
|
||||
latestPayload = null;
|
||||
renderJsonViewer("#sqlDiagJson", { error: message });
|
||||
frontendError("kb_app_demo.frontend.demo_sql_diag", `SQL diagnostics loading failed: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function copyDiagnostics(): Promise<void> {
|
||||
if (latestPayload) {
|
||||
await navigator.clipboard.writeText(jsonText(latestPayload));
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
installFrontendConsoleBridge("kb_app_demo.frontend.demo_sql_diag");
|
||||
frontendDebug("kb_app_demo.frontend.demo_sql_diag", "SQL diagnostics demo window loaded");
|
||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
|
||||
document.querySelector<HTMLButtonElement>("#refreshSqlDiagButton")?.addEventListener("click", () => {
|
||||
void refreshDiagnostics();
|
||||
});
|
||||
document.querySelector<HTMLButtonElement>("#copySqlDiagButton")?.addEventListener("click", () => {
|
||||
void copyDiagnostics();
|
||||
});
|
||||
void refreshDiagnostics();
|
||||
});
|
||||
42
kb-app-demo-desktop/frontend/ts/demo_sql_pg_core.ts
Normal file
42
kb-app-demo-desktop/frontend/ts/demo_sql_pg_core.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
// file: kb-app-demo-desktop/frontend/ts/demo_sql_pg_core.ts
|
||||
// version: 3
|
||||
|
||||
import * as bootstrap from "bootstrap";
|
||||
import "simplebar";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log";
|
||||
import { renderJsonViewer, renderTables, setText } from "./demo_sql_tables";
|
||||
import type { DemoSqlPgCorePayload } from "./bindings/kb_app_demo_desktop/demo_sql_pg_core/DemoSqlPgCorePayload";
|
||||
|
||||
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
|
||||
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
|
||||
|
||||
async function refreshCore(): Promise<void> {
|
||||
setText("#sqlCoreStatus", "Chargement...");
|
||||
try {
|
||||
const payload = await invoke<DemoSqlPgCorePayload>("load_demo_sql_pg_core");
|
||||
setText("#sqlCoreProfile", payload.activeProfileName);
|
||||
setText("#sqlCoreDsn", payload.maskedDsn);
|
||||
setText("#sqlCoreStatus", "OK");
|
||||
renderTables("#sqlCoreTableBody", payload.tables);
|
||||
renderJsonViewer("#sqlCoreJson", payload);
|
||||
frontendDebug("kb_app_demo.frontend.demo_sql_pg_core", "PostgreSQL core diagnostics refreshed");
|
||||
} catch (caughtError) {
|
||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||
setText("#sqlCoreStatus", `Erreur : ${message}`);
|
||||
renderJsonViewer("#sqlCoreJson", { error: message });
|
||||
frontendError("kb_app_demo.frontend.demo_sql_pg_core", `PostgreSQL core diagnostics loading failed: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
installFrontendConsoleBridge("kb_app_demo.frontend.demo_sql_pg_core");
|
||||
frontendDebug("kb_app_demo.frontend.demo_sql_pg_core", "PostgreSQL core demo window loaded");
|
||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
|
||||
document.querySelector<HTMLButtonElement>("#refreshSqlCoreButton")?.addEventListener("click", () => {
|
||||
void refreshCore();
|
||||
});
|
||||
void refreshCore();
|
||||
});
|
||||
42
kb-app-demo-desktop/frontend/ts/demo_sql_pg_raw.ts
Normal file
42
kb-app-demo-desktop/frontend/ts/demo_sql_pg_raw.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
// file: kb-app-demo-desktop/frontend/ts/demo_sql_pg_raw.ts
|
||||
// version: 3
|
||||
|
||||
import * as bootstrap from "bootstrap";
|
||||
import "simplebar";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log";
|
||||
import { renderJsonViewer, renderTables, setText } from "./demo_sql_tables";
|
||||
import type { DemoSqlPgRawPayload } from "./bindings/kb_app_demo_desktop/demo_sql_pg_raw/DemoSqlPgRawPayload";
|
||||
|
||||
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
|
||||
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
|
||||
|
||||
async function refreshRaw(): Promise<void> {
|
||||
setText("#sqlRawStatus", "Chargement...");
|
||||
try {
|
||||
const payload = await invoke<DemoSqlPgRawPayload>("load_demo_sql_pg_raw");
|
||||
setText("#sqlRawProfile", payload.activeProfileName);
|
||||
setText("#sqlRawDsn", payload.maskedDsn);
|
||||
setText("#sqlRawStatus", "OK");
|
||||
renderTables("#sqlRawTableBody", payload.tables);
|
||||
renderJsonViewer("#sqlRawJson", payload);
|
||||
frontendDebug("kb_app_demo.frontend.demo_sql_pg_raw", "PostgreSQL raw diagnostics refreshed");
|
||||
} catch (caughtError) {
|
||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||
setText("#sqlRawStatus", `Erreur : ${message}`);
|
||||
renderJsonViewer("#sqlRawJson", { error: message });
|
||||
frontendError("kb_app_demo.frontend.demo_sql_pg_raw", `PostgreSQL raw diagnostics loading failed: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
installFrontendConsoleBridge("kb_app_demo.frontend.demo_sql_pg_raw");
|
||||
frontendDebug("kb_app_demo.frontend.demo_sql_pg_raw", "PostgreSQL raw demo window loaded");
|
||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
|
||||
document.querySelector<HTMLButtonElement>("#refreshSqlRawButton")?.addEventListener("click", () => {
|
||||
void refreshRaw();
|
||||
});
|
||||
void refreshRaw();
|
||||
});
|
||||
800
kb-app-demo-desktop/frontend/ts/demo_sql_replay_candidates.ts
Normal file
800
kb-app-demo-desktop/frontend/ts/demo_sql_replay_candidates.ts
Normal file
@@ -0,0 +1,800 @@
|
||||
// file: kb-app-demo-desktop/frontend/ts/demo_sql_replay_candidates.ts
|
||||
// version: 7
|
||||
|
||||
import * as bootstrap from "bootstrap";
|
||||
import "simplebar";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
import DataTable, { type Api } from "datatables.net-bs5";
|
||||
import "datatables.net-bs5/css/dataTables.bootstrap5.css";
|
||||
import "datatables.net-select-bs5";
|
||||
import "datatables.net-select-bs5/css/select.bootstrap5.css";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log";
|
||||
import type { DemoSqlReplayEntityRequest } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayEntityRequest";
|
||||
import type { DemoSqlReplayEntityRow } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayEntityRow";
|
||||
import type { DemoSqlReplayKnownProgramOption } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayKnownProgramOption";
|
||||
import type { DemoSqlReplayOptionsPayload } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayOptionsPayload";
|
||||
import type { DemoSqlReplayProgramRequest } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayProgramRequest";
|
||||
import type { DemoSqlReplayProgramRow } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayProgramRow";
|
||||
import type { DemoSqlReplayTransactionRequest } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayTransactionRequest";
|
||||
import type { DemoSqlReplayTransactionRow } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayTransactionRow";
|
||||
|
||||
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
|
||||
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
|
||||
|
||||
type EntityKindCode = "mint" | "owner" | "account_key";
|
||||
|
||||
type EntityViewConfig = {
|
||||
kind: EntityKindCode;
|
||||
label: string;
|
||||
pluralLabel: string;
|
||||
tableSelector: string;
|
||||
containsSelector: string;
|
||||
limitSelector: string;
|
||||
summarySelector: string;
|
||||
loadButtonSelector: string;
|
||||
resetButtonSelector: string;
|
||||
useButtonSelector: string;
|
||||
copyButtonSelector: string;
|
||||
exportButtonSelector: string;
|
||||
fileName: string;
|
||||
};
|
||||
|
||||
const tracingTarget = "kb_app_demo.frontend.demo_sql_replay_candidates";
|
||||
const entityViewConfigs: EntityViewConfig[] = [
|
||||
{
|
||||
kind: "mint",
|
||||
label: "mint",
|
||||
pluralLabel: "mints",
|
||||
tableSelector: "#mintCandidateTable",
|
||||
containsSelector: "#mintContainsInput",
|
||||
limitSelector: "#mintLimitInput",
|
||||
summarySelector: "#mintResultSummary",
|
||||
loadButtonSelector: "#loadMintsButton",
|
||||
resetButtonSelector: "#resetMintsButton",
|
||||
useButtonSelector: "#useSelectedMintsButton",
|
||||
copyButtonSelector: "#copyMintsButton",
|
||||
exportButtonSelector: "#exportMintsCsvButton",
|
||||
fileName: "replay_mints.csv",
|
||||
},
|
||||
{
|
||||
kind: "owner",
|
||||
label: "owner",
|
||||
pluralLabel: "owners",
|
||||
tableSelector: "#ownerCandidateTable",
|
||||
containsSelector: "#ownerContainsInput",
|
||||
limitSelector: "#ownerLimitInput",
|
||||
summarySelector: "#ownerResultSummary",
|
||||
loadButtonSelector: "#loadOwnersButton",
|
||||
resetButtonSelector: "#resetOwnersButton",
|
||||
useButtonSelector: "#useSelectedOwnersButton",
|
||||
copyButtonSelector: "#copyOwnersButton",
|
||||
exportButtonSelector: "#exportOwnersCsvButton",
|
||||
fileName: "replay_owners.csv",
|
||||
},
|
||||
{
|
||||
kind: "account_key",
|
||||
label: "compte",
|
||||
pluralLabel: "comptes",
|
||||
tableSelector: "#accountCandidateTable",
|
||||
containsSelector: "#accountContainsInput",
|
||||
limitSelector: "#accountLimitInput",
|
||||
summarySelector: "#accountResultSummary",
|
||||
loadButtonSelector: "#loadAccountsButton",
|
||||
resetButtonSelector: "#resetAccountsButton",
|
||||
useButtonSelector: "#useSelectedAccountsButton",
|
||||
copyButtonSelector: "#copyAccountsButton",
|
||||
exportButtonSelector: "#exportAccountsCsvButton",
|
||||
fileName: "replay_account_keys.csv",
|
||||
},
|
||||
];
|
||||
let transactionTable: Api<DemoSqlReplayTransactionRow> | null = null;
|
||||
let programTable: Api<DemoSqlReplayProgramRow> | null = null;
|
||||
let mintTable: Api<DemoSqlReplayEntityRow> | null = null;
|
||||
let ownerTable: Api<DemoSqlReplayEntityRow> | null = null;
|
||||
let accountTable: Api<DemoSqlReplayEntityRow> | null = null;
|
||||
let busyOperationCount = 0;
|
||||
let maximumReplayCandidateLimit = 5_000;
|
||||
|
||||
function element<T extends HTMLElement>(selector: string): T {
|
||||
const value = document.querySelector<T>(selector);
|
||||
if (!value) {
|
||||
throw new Error(`Missing UI element: ${selector}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function optionalInputValue(selector: string): string | null {
|
||||
const value = element<HTMLInputElement | HTMLSelectElement>(selector).value.trim();
|
||||
return value.length === 0 ? null : value;
|
||||
}
|
||||
|
||||
function requiredInputValue(selector: string): string {
|
||||
const value = optionalInputValue(selector);
|
||||
if (value === null) {
|
||||
throw new Error(`Valeur obligatoire manquante pour ${selector}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function positiveIntegerValue(selector: string): number {
|
||||
const value = Number.parseInt(requiredInputValue(selector), 10);
|
||||
if (!Number.isSafeInteger(value) || value <= 0) {
|
||||
throw new Error(`Valeur entière positive invalide pour ${selector}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function boundedPositiveIntegerValue(selector: string): number {
|
||||
const value = positiveIntegerValue(selector);
|
||||
if (value > maximumReplayCandidateLimit) {
|
||||
throw new Error(
|
||||
`La limite doit être comprise entre 1 et ${maximumReplayCandidateLimit} pour ${selector}`,
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function optionalNonNegativeIntegerValue(selector: string): number | null {
|
||||
const raw = optionalInputValue(selector);
|
||||
if (raw === null) {
|
||||
return null;
|
||||
}
|
||||
const value = Number.parseInt(raw, 10);
|
||||
if (!Number.isSafeInteger(value) || value < 0) {
|
||||
throw new Error(`Valeur entière positive ou nulle invalide pour ${selector}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function escapeHtml(value: unknown): string {
|
||||
const text = String(value ?? "");
|
||||
return text
|
||||
.split("&").join("&")
|
||||
.split("<").join("<")
|
||||
.split(">").join(">")
|
||||
.split('"').join(""")
|
||||
.split("'").join("'");
|
||||
}
|
||||
|
||||
function codeDisplay(value: unknown, type: string): string {
|
||||
if (type !== "display") {
|
||||
return String(value ?? "");
|
||||
}
|
||||
return `<code class="text-break">${escapeHtml(value)}</code>`;
|
||||
}
|
||||
|
||||
function compactIdentifier(value: string, leadingLength: number = 12, trailingLength: number = 10): string {
|
||||
if (value.length <= leadingLength + trailingLength + 1) {
|
||||
return value;
|
||||
}
|
||||
return `${value.slice(0, leadingLength)}…${value.slice(-trailingLength)}`;
|
||||
}
|
||||
|
||||
function copyableCodeDisplay(label: string): (value: unknown, type: string) => string {
|
||||
return (value: unknown, type: string): string => {
|
||||
const text = String(value ?? "");
|
||||
if (type !== "display") {
|
||||
return text;
|
||||
}
|
||||
const escapedText = escapeHtml(text);
|
||||
const compactText = escapeHtml(compactIdentifier(text));
|
||||
const escapedLabel = escapeHtml(label);
|
||||
return `<span class="d-inline-flex align-items-center gap-1 text-nowrap"><code data-bs-toggle="tooltip" data-bs-placement="top" title="${escapedText}">${compactText}</code><button class="btn btn-sm btn-link text-secondary p-0 replay-cell-copy" type="button" data-bs-toggle="tooltip" data-bs-placement="top" title="Copier ${escapedLabel}" aria-label="Copier ${escapedLabel}" data-copy-value="${escapedText}" data-copy-label="${escapedLabel}"><i class="fa-regular fa-copy" aria-hidden="true"></i></button></span>`;
|
||||
};
|
||||
}
|
||||
|
||||
function badgeDisplay(value: unknown, type: string): string {
|
||||
if (type !== "display") {
|
||||
return String(value ?? "");
|
||||
}
|
||||
const text = String(value ?? "—");
|
||||
const className = text === "succeeded" || text === "core_extracted"
|
||||
? "text-bg-success"
|
||||
: text === "failed"
|
||||
? "text-bg-danger"
|
||||
: text === "running" || text === "received"
|
||||
? "text-bg-warning"
|
||||
: "text-bg-secondary";
|
||||
return `<span class="badge ${className}">${escapeHtml(text)}</span>`;
|
||||
}
|
||||
|
||||
function coreStatusText(row: DemoSqlReplayTransactionRow): string {
|
||||
if (!row.hasCoreTransaction) {
|
||||
return "absent";
|
||||
}
|
||||
if (row.transactionFailed === true) {
|
||||
return "échec on-chain";
|
||||
}
|
||||
return "présent";
|
||||
}
|
||||
|
||||
function coreDisplay(_value: unknown, type: string, row: DemoSqlReplayTransactionRow): string | number {
|
||||
if (type === "display") {
|
||||
if (!row.hasCoreTransaction) {
|
||||
return '<span class="badge text-bg-secondary">absent</span>';
|
||||
}
|
||||
if (row.transactionFailed === true) {
|
||||
return '<span class="badge text-bg-danger" title="La transaction Solana a échoué on-chain, mais son graphe core a été extrait.">échec on-chain</span>';
|
||||
}
|
||||
return '<span class="badge text-bg-success">présent</span>';
|
||||
}
|
||||
if (type === "filter") {
|
||||
return coreStatusText(row);
|
||||
}
|
||||
if (row.transactionFailed === true) {
|
||||
return 0;
|
||||
}
|
||||
return row.hasCoreTransaction ? 1 : 2;
|
||||
}
|
||||
|
||||
function occurrenceRatioDisplay(_value: unknown, type: string, row: DemoSqlReplayEntityRow): string | number {
|
||||
const ratio = row.transactionCount > 0 ? row.occurrenceCount / row.transactionCount : 0;
|
||||
if (type === "display" || type === "filter") {
|
||||
return ratio.toFixed(2);
|
||||
}
|
||||
return ratio;
|
||||
}
|
||||
|
||||
function slotSpanDisplay(_value: unknown, type: string, row: DemoSqlReplayEntityRow): string | number {
|
||||
const span = Math.max(0, row.maxSlot - row.minSlot);
|
||||
if (type === "display" || type === "filter") {
|
||||
return String(span);
|
||||
}
|
||||
return span;
|
||||
}
|
||||
|
||||
function commonLanguage(emptyTable: string, infoLabel: string, zeroRecords: string): object {
|
||||
return {
|
||||
emptyTable,
|
||||
info: `_START_ à _END_ sur _TOTAL_ ${infoLabel}`,
|
||||
infoEmpty: `0 ${infoLabel}`,
|
||||
infoFiltered: "(filtré depuis _MAX_)",
|
||||
lengthMenu: "Afficher _MENU_",
|
||||
search: "Filtre local :",
|
||||
zeroRecords,
|
||||
paginate: { first: "Premier", last: "Dernier", next: "Suivant", previous: "Précédent" },
|
||||
select: { rows: { _: "%d lignes sélectionnées", 0: "", 1: "1 ligne sélectionnée" } },
|
||||
};
|
||||
}
|
||||
|
||||
function createTransactionTable(): Api<DemoSqlReplayTransactionRow> {
|
||||
return new DataTable<DemoSqlReplayTransactionRow>("#transactionCandidateTable", {
|
||||
data: [],
|
||||
columns: [
|
||||
{
|
||||
data: null,
|
||||
className: "replay-select-column text-center",
|
||||
width: "1%",
|
||||
orderable: false,
|
||||
searchable: false,
|
||||
render: DataTable.render.select(),
|
||||
},
|
||||
{ data: "signature", render: copyableCodeDisplay("la signature") },
|
||||
{ data: "slot" },
|
||||
{ data: "rawProcessingState", render: badgeDisplay },
|
||||
{ data: "hasCoreTransaction", render: coreDisplay },
|
||||
{ data: "ledgerStatus", render: badgeDisplay },
|
||||
{ data: "processorVersion", defaultContent: "—", render: codeDisplay },
|
||||
{ data: "attemptCount" },
|
||||
{ data: "outerInstructionCount" },
|
||||
{ data: "innerInstructionCount" },
|
||||
{ data: "outerProgramCount" },
|
||||
{ data: "innerProgramCount" },
|
||||
{ data: "updatedAt" },
|
||||
],
|
||||
order: [[2, "desc"]],
|
||||
pageLength: 10,
|
||||
lengthMenu: [10, 25, 50, 100, 250],
|
||||
select: { style: "multi", selector: "td:first-child", headerCheckbox: "select-all" },
|
||||
language: commonLanguage(
|
||||
"Aucune transaction chargée",
|
||||
"transactions",
|
||||
"Aucune transaction ne correspond au filtre local",
|
||||
),
|
||||
drawCallback: () => initializeTooltips(),
|
||||
});
|
||||
}
|
||||
|
||||
function createProgramTable(): Api<DemoSqlReplayProgramRow> {
|
||||
return new DataTable<DemoSqlReplayProgramRow>("#programCandidateTable", {
|
||||
data: [],
|
||||
columns: [
|
||||
{
|
||||
data: null,
|
||||
className: "replay-select-column text-center",
|
||||
width: "1%",
|
||||
orderable: false,
|
||||
searchable: false,
|
||||
render: DataTable.render.select(),
|
||||
},
|
||||
{ data: "programCode", defaultContent: "—", render: codeDisplay },
|
||||
{ data: "programId", render: copyableCodeDisplay("le program ID") },
|
||||
{ data: "transactionCount" },
|
||||
{ data: "outerInstructionCount" },
|
||||
{ data: "innerInstructionCount" },
|
||||
{ data: "logCount" },
|
||||
{ data: "minSlot" },
|
||||
{ data: "maxSlot" },
|
||||
],
|
||||
order: [[3, "desc"]],
|
||||
pageLength: 10,
|
||||
lengthMenu: [10, 25, 50, 100, 250],
|
||||
select: { style: "multi", selector: "td:first-child", headerCheckbox: "select-all" },
|
||||
language: commonLanguage(
|
||||
"Aucun programme chargé",
|
||||
"programmes",
|
||||
"Aucun programme ne correspond au filtre local",
|
||||
),
|
||||
drawCallback: () => initializeTooltips(),
|
||||
});
|
||||
}
|
||||
|
||||
function createEntityTable(config: EntityViewConfig): Api<DemoSqlReplayEntityRow> {
|
||||
return new DataTable<DemoSqlReplayEntityRow>(config.tableSelector, {
|
||||
data: [],
|
||||
columns: [
|
||||
{
|
||||
data: null,
|
||||
className: "replay-select-column text-center",
|
||||
width: "1%",
|
||||
orderable: false,
|
||||
searchable: false,
|
||||
render: DataTable.render.select(),
|
||||
},
|
||||
{ data: "entityValue", render: copyableCodeDisplay(config.label) },
|
||||
{ data: "transactionCount" },
|
||||
{ data: "occurrenceCount" },
|
||||
{ data: null, render: occurrenceRatioDisplay },
|
||||
{ data: "minSlot" },
|
||||
{ data: "maxSlot" },
|
||||
{ data: null, render: slotSpanDisplay },
|
||||
],
|
||||
order: [[2, "desc"]],
|
||||
pageLength: 10,
|
||||
lengthMenu: [10, 25, 50, 100, 250],
|
||||
select: { style: "multi", selector: "td:first-child", headerCheckbox: "select-all" },
|
||||
language: commonLanguage(
|
||||
`Aucun ${config.label} chargé`,
|
||||
config.pluralLabel,
|
||||
`Aucun ${config.label} ne correspond au filtre local`,
|
||||
),
|
||||
drawCallback: () => initializeTooltips(),
|
||||
});
|
||||
}
|
||||
|
||||
function entityTable(kind: EntityKindCode): Api<DemoSqlReplayEntityRow> | null {
|
||||
if (kind === "mint") {
|
||||
return mintTable;
|
||||
}
|
||||
if (kind === "owner") {
|
||||
return ownerTable;
|
||||
}
|
||||
return accountTable;
|
||||
}
|
||||
|
||||
function replaceRows<T>(table: Api<T>, rows: T[]): void {
|
||||
table.clear();
|
||||
table.rows.add(rows);
|
||||
table.draw();
|
||||
}
|
||||
|
||||
function resetTableState<T>(table: Api<T> | null): void {
|
||||
if (table === null) {
|
||||
return;
|
||||
}
|
||||
table.search("");
|
||||
table.columns().search("");
|
||||
table.rows().deselect();
|
||||
table.draw();
|
||||
}
|
||||
|
||||
function selectedOrFilteredRows<T>(table: Api<T>): T[] {
|
||||
const selected = table.rows({ selected: true, search: "applied" }).data().toArray();
|
||||
if (selected.length > 0) {
|
||||
return selected;
|
||||
}
|
||||
return table.rows({ search: "applied" }).data().toArray();
|
||||
}
|
||||
|
||||
function selectedOrSingleFilteredRow<T>(table: Api<T>, label: string): T {
|
||||
const selected = table.rows({ selected: true }).data().toArray();
|
||||
if (selected.length === 1) {
|
||||
return selected[0];
|
||||
}
|
||||
if (selected.length > 1) {
|
||||
throw new Error(`Plusieurs ${label} sont cochés. Conservez une seule sélection.`);
|
||||
}
|
||||
const filtered = table.rows({ search: "applied" }).data().toArray();
|
||||
if (filtered.length === 1) {
|
||||
return filtered[0];
|
||||
}
|
||||
throw new Error(`Cochez exactement un ${label}, ou réduisez le filtre local à une seule ligne.`);
|
||||
}
|
||||
|
||||
function setBusy(busy: boolean, message: string): void {
|
||||
busyOperationCount = busy ? busyOperationCount + 1 : Math.max(0, busyOperationCount - 1);
|
||||
const operationInProgress = busyOperationCount > 0;
|
||||
const badge = element<HTMLElement>("#replayCandidateStatusBadge");
|
||||
badge.textContent = operationInProgress ? (busy ? message : "Chargement en cours") : "Prêt";
|
||||
badge.className = operationInProgress ? "badge text-bg-warning" : "badge text-bg-success";
|
||||
document.querySelectorAll<HTMLButtonElement>("button").forEach(button => {
|
||||
if (button.id !== "openCoreExtractionButton") {
|
||||
button.disabled = operationInProgress;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function buildTransactionRequest(): DemoSqlReplayTransactionRequest {
|
||||
const entityKind = optionalInputValue("#transactionEntityKindSelect");
|
||||
const entityValue = optionalInputValue("#transactionEntityValueInput");
|
||||
if ((entityKind === null) !== (entityValue === null)) {
|
||||
throw new Error("Le type d’entité et sa valeur doivent être renseignés ensemble.");
|
||||
}
|
||||
return {
|
||||
signatureContains: optionalInputValue("#transactionSignatureContainsInput"),
|
||||
minSlot: optionalNonNegativeIntegerValue("#transactionMinSlotInput"),
|
||||
maxSlot: optionalNonNegativeIntegerValue("#transactionMaxSlotInput"),
|
||||
rawProcessingState: optionalInputValue("#transactionRawStateSelect"),
|
||||
ledgerStatus: optionalInputValue("#transactionLedgerStatusSelect"),
|
||||
programId: optionalInputValue("#transactionProgramIdInput"),
|
||||
programScope: requiredInputValue("#transactionProgramScopeSelect"),
|
||||
entityKind,
|
||||
entityValue,
|
||||
limit: boundedPositiveIntegerValue("#transactionLimitInput"),
|
||||
newestFirst: element<HTMLInputElement>("#transactionNewestFirstInput").checked,
|
||||
};
|
||||
}
|
||||
|
||||
async function resetTransactionFilters(): Promise<void> {
|
||||
element<HTMLInputElement>("#transactionSignatureContainsInput").value = "";
|
||||
element<HTMLInputElement>("#transactionMinSlotInput").value = "";
|
||||
element<HTMLInputElement>("#transactionMaxSlotInput").value = "";
|
||||
element<HTMLSelectElement>("#transactionRawStateSelect").value = "";
|
||||
element<HTMLSelectElement>("#transactionLedgerStatusSelect").value = "";
|
||||
element<HTMLInputElement>("#transactionProgramIdInput").value = "";
|
||||
element<HTMLSelectElement>("#transactionProgramScopeSelect").value = "any";
|
||||
element<HTMLSelectElement>("#transactionEntityKindSelect").value = "";
|
||||
element<HTMLInputElement>("#transactionEntityValueInput").value = "";
|
||||
element<HTMLInputElement>("#transactionLimitInput").value = "500";
|
||||
element<HTMLInputElement>("#transactionNewestFirstInput").checked = true;
|
||||
resetTableState(transactionTable);
|
||||
await loadTransactions();
|
||||
}
|
||||
|
||||
async function resetProgramFilters(): Promise<void> {
|
||||
element<HTMLSelectElement>("#knownProgramSelect").value = "";
|
||||
element<HTMLInputElement>("#programContainsInput").value = "";
|
||||
element<HTMLInputElement>("#programLimitInput").value = "500";
|
||||
resetTableState(programTable);
|
||||
await loadPrograms();
|
||||
}
|
||||
|
||||
async function resetEntityFilters(config: EntityViewConfig): Promise<void> {
|
||||
element<HTMLInputElement>(config.containsSelector).value = "";
|
||||
element<HTMLInputElement>(config.limitSelector).value = "500";
|
||||
resetTableState(entityTable(config.kind));
|
||||
await loadEntity(config);
|
||||
}
|
||||
|
||||
async function loadTransactions(): Promise<void> {
|
||||
if (transactionTable === null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setBusy(true, "Chargement transactions");
|
||||
const request = buildTransactionRequest();
|
||||
const rows = await invoke<DemoSqlReplayTransactionRow[]>("load_demo_sql_replay_transactions", { request });
|
||||
replaceRows(transactionTable, rows);
|
||||
element<HTMLElement>("#transactionResultSummary").textContent = `${rows.length} transaction(s) chargée(s) depuis PostgreSQL.`;
|
||||
frontendDebug(tracingTarget, `loaded ${rows.length} replay transaction candidates`);
|
||||
} catch (caughtError) {
|
||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||
element<HTMLElement>("#transactionResultSummary").textContent = `Erreur : ${message}`;
|
||||
frontendError(tracingTarget, `transaction candidate loading failed: ${message}`);
|
||||
} finally {
|
||||
setBusy(false, "Prêt");
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPrograms(): Promise<void> {
|
||||
if (programTable === null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setBusy(true, "Chargement programmes");
|
||||
const request: DemoSqlReplayProgramRequest = {
|
||||
programIdContains: optionalInputValue("#programContainsInput"),
|
||||
limit: boundedPositiveIntegerValue("#programLimitInput"),
|
||||
};
|
||||
const rows = await invoke<DemoSqlReplayProgramRow[]>("load_demo_sql_replay_programs", { request });
|
||||
replaceRows(programTable, rows);
|
||||
element<HTMLElement>("#programResultSummary").textContent = `${rows.length} programme(s) chargé(s), avec occurrences outer, inner et logs liés.`;
|
||||
frontendDebug(tracingTarget, `loaded ${rows.length} replay program summaries`);
|
||||
} catch (caughtError) {
|
||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||
element<HTMLElement>("#programResultSummary").textContent = `Erreur : ${message}`;
|
||||
frontendError(tracingTarget, `program summary loading failed: ${message}`);
|
||||
} finally {
|
||||
setBusy(false, "Prêt");
|
||||
}
|
||||
}
|
||||
|
||||
async function loadEntity(config: EntityViewConfig): Promise<void> {
|
||||
const table = entityTable(config.kind);
|
||||
if (table === null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setBusy(true, `Chargement ${config.pluralLabel}`);
|
||||
const request: DemoSqlReplayEntityRequest = {
|
||||
entityKind: config.kind,
|
||||
entityValueContains: optionalInputValue(config.containsSelector),
|
||||
limit: boundedPositiveIntegerValue(config.limitSelector),
|
||||
};
|
||||
const rows = await invoke<DemoSqlReplayEntityRow[]>("load_demo_sql_replay_entities", { request });
|
||||
replaceRows(table, rows);
|
||||
element<HTMLElement>(config.summarySelector).textContent = `${rows.length} ${config.pluralLabel} chargé(s) depuis PostgreSQL.`;
|
||||
frontendDebug(tracingTarget, `loaded ${rows.length} replay ${config.kind} summaries`);
|
||||
} catch (caughtError) {
|
||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||
element<HTMLElement>(config.summarySelector).textContent = `Erreur : ${message}`;
|
||||
frontendError(tracingTarget, `${config.kind} summary loading failed: ${message}`);
|
||||
} finally {
|
||||
setBusy(false, "Prêt");
|
||||
}
|
||||
}
|
||||
|
||||
async function copyLines(lines: string[], label: string): Promise<void> {
|
||||
if (lines.length === 0) {
|
||||
throw new Error(`Aucune ${label} à copier.`);
|
||||
}
|
||||
await navigator.clipboard.writeText(lines.join("\n"));
|
||||
element<HTMLElement>("#replayCandidateStatusBadge").textContent = `${lines.length} ${label} copiée(s)`;
|
||||
}
|
||||
|
||||
function csvCell(value: unknown): string {
|
||||
const text = value === null || value === undefined ? "" : String(value);
|
||||
return `"${text.split('"').join('""')}"`;
|
||||
}
|
||||
|
||||
function csvContent(headers: string[], rows: unknown[][]): string {
|
||||
const lines = [headers, ...rows].map(row => row.map(csvCell).join(";"));
|
||||
return `${lines.join("\r\n")}\r\n`;
|
||||
}
|
||||
|
||||
async function saveCsv(fileName: string, content: string, rowCount: number): Promise<void> {
|
||||
if (rowCount === 0) {
|
||||
throw new Error("Aucune ligne à exporter.");
|
||||
}
|
||||
const path = await invoke<string>("export_demo_sql_replay_csv", { fileName, content });
|
||||
const badge = element<HTMLElement>("#replayCandidateStatusBadge");
|
||||
badge.textContent = `${rowCount} ligne(s) exportée(s) : ${path}`;
|
||||
badge.className = "badge text-bg-success";
|
||||
frontendDebug(tracingTarget, `CSV export written: ${path}`);
|
||||
}
|
||||
|
||||
async function exportTransactionsCsv(): Promise<void> {
|
||||
const rows = transactionRowsForExport();
|
||||
const content = csvContent(
|
||||
["signature", "slot", "raw_processing_state", "retention_state", "core_status", "ledger_status", "processor_version", "attempt_count", "outer_instruction_count", "inner_instruction_count", "outer_program_count", "inner_program_count", "updated_at"],
|
||||
rows.map(row => [row.signature, row.slot, row.rawProcessingState, row.retentionState, coreStatusText(row), row.ledgerStatus, row.processorVersion, row.attemptCount, row.outerInstructionCount, row.innerInstructionCount, row.outerProgramCount, row.innerProgramCount, row.updatedAt]),
|
||||
);
|
||||
await saveCsv("replay_transactions.csv", content, rows.length);
|
||||
}
|
||||
|
||||
async function exportProgramsCsv(): Promise<void> {
|
||||
const rows = programRowsForExport();
|
||||
const content = csvContent(
|
||||
["program_code", "program_id", "transaction_count", "outer_instruction_count", "inner_instruction_count", "log_count", "min_slot", "max_slot"],
|
||||
rows.map(row => [row.programCode, row.programId, row.transactionCount, row.outerInstructionCount, row.innerInstructionCount, row.logCount, row.minSlot, row.maxSlot]),
|
||||
);
|
||||
await saveCsv("replay_programs.csv", content, rows.length);
|
||||
}
|
||||
|
||||
async function exportEntityCsv(config: EntityViewConfig): Promise<void> {
|
||||
const rows = entityRowsForExport(config.kind);
|
||||
const content = csvContent(
|
||||
["entity_kind", "entity_value", "transaction_count", "occurrence_count", "occurrences_per_transaction", "min_slot", "max_slot", "slot_span"],
|
||||
rows.map(row => [row.entityKind, row.entityValue, row.transactionCount, row.occurrenceCount, row.transactionCount > 0 ? row.occurrenceCount / row.transactionCount : 0, row.minSlot, row.maxSlot, Math.max(0, row.maxSlot - row.minSlot)]),
|
||||
);
|
||||
await saveCsv(config.fileName, content, rows.length);
|
||||
}
|
||||
|
||||
function transactionRowsForExport(): DemoSqlReplayTransactionRow[] {
|
||||
if (transactionTable === null) {
|
||||
return [];
|
||||
}
|
||||
return selectedOrFilteredRows(transactionTable);
|
||||
}
|
||||
|
||||
function programRowsForExport(): DemoSqlReplayProgramRow[] {
|
||||
if (programTable === null) {
|
||||
return [];
|
||||
}
|
||||
return selectedOrFilteredRows(programTable);
|
||||
}
|
||||
|
||||
function entityRowsForExport(kind: EntityKindCode): DemoSqlReplayEntityRow[] {
|
||||
const table = entityTable(kind);
|
||||
if (table === null) {
|
||||
return [];
|
||||
}
|
||||
return selectedOrFilteredRows(table);
|
||||
}
|
||||
|
||||
function showTransactionsTab(): void {
|
||||
bootstrap.Tab.getOrCreateInstance(element<HTMLButtonElement>("#transactionsTabButton")).show();
|
||||
}
|
||||
|
||||
async function useSelectedProgram(): Promise<void> {
|
||||
if (programTable === null) {
|
||||
return;
|
||||
}
|
||||
const row = selectedOrSingleFilteredRow(programTable, "programme");
|
||||
element<HTMLInputElement>("#transactionProgramIdInput").value = row.programId;
|
||||
element<HTMLSelectElement>("#transactionProgramScopeSelect").value = "any";
|
||||
showTransactionsTab();
|
||||
await loadTransactions();
|
||||
}
|
||||
|
||||
async function useSelectedEntity(config: EntityViewConfig): Promise<void> {
|
||||
const table = entityTable(config.kind);
|
||||
if (table === null) {
|
||||
return;
|
||||
}
|
||||
const row = selectedOrSingleFilteredRow(table, config.label);
|
||||
element<HTMLSelectElement>("#transactionEntityKindSelect").value = config.kind;
|
||||
element<HTMLInputElement>("#transactionEntityValueInput").value = row.entityValue;
|
||||
showTransactionsTab();
|
||||
await loadTransactions();
|
||||
}
|
||||
|
||||
async function openCoreExtraction(): Promise<void> {
|
||||
try {
|
||||
await invoke("open_demo_core_extraction_window");
|
||||
} catch (caughtError) {
|
||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||
frontendError(tracingTarget, `core extraction window opening failed: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadOptions(): Promise<void> {
|
||||
const options = await invoke<DemoSqlReplayOptionsPayload>("demo_sql_replay_options");
|
||||
if (!Number.isSafeInteger(options.maximumLimit) || options.maximumLimit <= 0) {
|
||||
throw new Error("La limite maximale des candidats replay retournée par le backend est invalide.");
|
||||
}
|
||||
maximumReplayCandidateLimit = options.maximumLimit;
|
||||
element<HTMLElement>("#replayCandidateProfileBadge").textContent = `Profil ${options.activeProfileName}`;
|
||||
element<HTMLElement>("#replayCandidateDsn").textContent = options.maskedDsn;
|
||||
element<HTMLElement>("#replayCandidateMaximumLimit").textContent = String(options.maximumLimit);
|
||||
document.querySelectorAll<HTMLInputElement>("input[type=number][max]").forEach(input => {
|
||||
input.max = String(options.maximumLimit);
|
||||
});
|
||||
populateKnownPrograms(options.knownPrograms);
|
||||
}
|
||||
|
||||
function populateKnownPrograms(programs: DemoSqlReplayKnownProgramOption[]): void {
|
||||
const select = element<HTMLSelectElement>("#knownProgramSelect");
|
||||
const datalist = element<HTMLDataListElement>("#knownProgramDatalist");
|
||||
select.replaceChildren(new Option("Tous les programmes connus", ""));
|
||||
datalist.replaceChildren();
|
||||
for (const program of programs) {
|
||||
const label = `${program.code} — ${program.programId}`;
|
||||
select.add(new Option(label, program.programId));
|
||||
const option = document.createElement("option");
|
||||
option.value = program.programId;
|
||||
option.label = program.code;
|
||||
datalist.append(option);
|
||||
}
|
||||
}
|
||||
|
||||
function initializeTooltips(): void {
|
||||
document.querySelectorAll<HTMLElement>('[data-bs-toggle="tooltip"]').forEach(target => {
|
||||
bootstrap.Tooltip.getOrCreateInstance(target);
|
||||
});
|
||||
}
|
||||
|
||||
function adjustVisibleTables(): void {
|
||||
transactionTable?.columns.adjust();
|
||||
programTable?.columns.adjust();
|
||||
mintTable?.columns.adjust();
|
||||
ownerTable?.columns.adjust();
|
||||
accountTable?.columns.adjust();
|
||||
}
|
||||
|
||||
function installInlineCopyHandler(): void {
|
||||
document.addEventListener("click", event => {
|
||||
const source = event.target;
|
||||
if (!(source instanceof Element)) {
|
||||
return;
|
||||
}
|
||||
const button = source.closest<HTMLButtonElement>("button[data-copy-value]");
|
||||
if (button === null) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const value = button.dataset.copyValue ?? "";
|
||||
const label = button.dataset.copyLabel ?? "valeur";
|
||||
void copyLines([value], label).catch(error => reportUiError(`copy ${label}`, error));
|
||||
});
|
||||
}
|
||||
|
||||
function reportUiError(action: string, caughtError: unknown): void {
|
||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||
element<HTMLElement>("#replayCandidateStatusBadge").textContent = `Erreur : ${message}`;
|
||||
element<HTMLElement>("#replayCandidateStatusBadge").className = "badge text-bg-danger";
|
||||
frontendError(tracingTarget, `${action} failed: ${message}`);
|
||||
}
|
||||
|
||||
function installHandlers(): void {
|
||||
element<HTMLButtonElement>("#loadTransactionsButton").addEventListener("click", () => void loadTransactions());
|
||||
element<HTMLButtonElement>("#resetTransactionsButton").addEventListener("click", () => {
|
||||
void resetTransactionFilters().catch(error => reportUiError("reset transaction filters", error));
|
||||
});
|
||||
element<HTMLButtonElement>("#loadProgramsButton").addEventListener("click", () => void loadPrograms());
|
||||
element<HTMLButtonElement>("#resetProgramsButton").addEventListener("click", () => {
|
||||
void resetProgramFilters().catch(error => reportUiError("reset program filters", error));
|
||||
});
|
||||
element<HTMLButtonElement>("#openCoreExtractionButton").addEventListener("click", () => void openCoreExtraction());
|
||||
element<HTMLButtonElement>("#copySignaturesButton").addEventListener("click", () => {
|
||||
void copyLines(transactionRowsForExport().map(row => row.signature), "signature").catch(error => reportUiError("copy signatures", error));
|
||||
});
|
||||
element<HTMLButtonElement>("#exportTransactionsCsvButton").addEventListener("click", () => {
|
||||
void exportTransactionsCsv().catch(error => reportUiError("export transactions CSV", error));
|
||||
});
|
||||
element<HTMLButtonElement>("#copyProgramsButton").addEventListener("click", () => {
|
||||
void copyLines(programRowsForExport().map(row => row.programId), "program ID").catch(error => reportUiError("copy programs", error));
|
||||
});
|
||||
element<HTMLButtonElement>("#exportProgramsCsvButton").addEventListener("click", () => {
|
||||
void exportProgramsCsv().catch(error => reportUiError("export programs CSV", error));
|
||||
});
|
||||
element<HTMLButtonElement>("#useSelectedProgramButton").addEventListener("click", () => {
|
||||
void useSelectedProgram().catch(error => reportUiError("use selected program", error));
|
||||
});
|
||||
for (const config of entityViewConfigs) {
|
||||
element<HTMLButtonElement>(config.loadButtonSelector).addEventListener("click", () => void loadEntity(config));
|
||||
element<HTMLButtonElement>(config.resetButtonSelector).addEventListener("click", () => {
|
||||
void resetEntityFilters(config).catch(error => reportUiError(`reset ${config.pluralLabel} filters`, error));
|
||||
});
|
||||
element<HTMLButtonElement>(config.copyButtonSelector).addEventListener("click", () => {
|
||||
void copyLines(entityRowsForExport(config.kind).map(row => row.entityValue), config.label).catch(error => reportUiError(`copy ${config.pluralLabel}`, error));
|
||||
});
|
||||
element<HTMLButtonElement>(config.exportButtonSelector).addEventListener("click", () => {
|
||||
void exportEntityCsv(config).catch(error => reportUiError(`export ${config.pluralLabel} CSV`, error));
|
||||
});
|
||||
element<HTMLButtonElement>(config.useButtonSelector).addEventListener("click", () => {
|
||||
void useSelectedEntity(config).catch(error => reportUiError(`use selected ${config.label}`, error));
|
||||
});
|
||||
}
|
||||
element<HTMLSelectElement>("#knownProgramSelect").addEventListener("change", event => {
|
||||
const target = event.currentTarget as HTMLSelectElement;
|
||||
element<HTMLInputElement>("#programContainsInput").value = target.value;
|
||||
});
|
||||
document.querySelectorAll<HTMLButtonElement>('[data-bs-toggle="tab"]').forEach(button => {
|
||||
button.addEventListener("shown.bs.tab", adjustVisibleTables);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
installFrontendConsoleBridge(tracingTarget);
|
||||
frontendDebug(tracingTarget, "SQL replay candidate browser loaded");
|
||||
transactionTable = createTransactionTable();
|
||||
programTable = createProgramTable();
|
||||
mintTable = createEntityTable(entityViewConfigs[0]);
|
||||
ownerTable = createEntityTable(entityViewConfigs[1]);
|
||||
accountTable = createEntityTable(entityViewConfigs[2]);
|
||||
initializeTooltips();
|
||||
installHandlers();
|
||||
installInlineCopyHandler();
|
||||
void loadOptions()
|
||||
.then(() => Promise.all([
|
||||
loadTransactions(),
|
||||
loadPrograms(),
|
||||
loadEntity(entityViewConfigs[0]),
|
||||
loadEntity(entityViewConfigs[1]),
|
||||
loadEntity(entityViewConfigs[2]),
|
||||
]))
|
||||
.catch(caughtError => reportUiError("initial loading", caughtError));
|
||||
});
|
||||
68
kb-app-demo-desktop/frontend/ts/demo_sql_tables.ts
Normal file
68
kb-app-demo-desktop/frontend/ts/demo_sql_tables.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
// file: kb-app-demo-desktop/frontend/ts/demo_sql_tables.ts
|
||||
// version: 2
|
||||
|
||||
import "@andypf/json-viewer";
|
||||
import type { DemoSqlTableSnapshot } from "./bindings/kb_app_demo_desktop/demo_sql/DemoSqlTableSnapshot";
|
||||
|
||||
export function setText(selector: string, value: string): void {
|
||||
const element = document.querySelector<HTMLElement>(selector);
|
||||
if (element) {
|
||||
element.textContent = value;
|
||||
}
|
||||
}
|
||||
|
||||
export function renderTables(selector: string, tables: DemoSqlTableSnapshot[]): void {
|
||||
const tbody = document.querySelector<HTMLTableSectionElement>(selector);
|
||||
if (!tbody) {
|
||||
return;
|
||||
}
|
||||
tbody.textContent = "";
|
||||
for (const table of tables) {
|
||||
const row = document.createElement("tr");
|
||||
row.appendChild(cell(table.tableName, true));
|
||||
row.appendChild(cell(table.domain, false));
|
||||
row.appendChild(cell(table.role, false));
|
||||
row.appendChild(cell(table.exists ? "oui" : "non", false));
|
||||
row.appendChild(cell(table.rowCount === null ? "—" : String(table.rowCount), false));
|
||||
row.appendChild(cell(table.minSlot === null ? "—" : String(table.minSlot), false));
|
||||
row.appendChild(cell(table.maxSlot === null ? "—" : String(table.maxSlot), false));
|
||||
row.appendChild(cell(table.latestCreatedAt ?? "—", false));
|
||||
tbody.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
export function jsonText(value: unknown): string {
|
||||
return JSON.stringify(value, null, 2);
|
||||
}
|
||||
|
||||
export function renderJsonViewer(selector: string, value: unknown): void {
|
||||
const container = document.querySelector<HTMLElement>(selector);
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
const viewer = document.createElement("andypf-json-viewer");
|
||||
viewer.setAttribute("indent", "2");
|
||||
viewer.setAttribute("expanded", "1");
|
||||
viewer.setAttribute("theme", "default-light");
|
||||
viewer.setAttribute("show-data-types", "true");
|
||||
viewer.setAttribute("show-toolbar", "false");
|
||||
viewer.setAttribute("expand-icon-type", "arrow");
|
||||
viewer.setAttribute("show-copy", "true");
|
||||
viewer.setAttribute("show-size", "true");
|
||||
viewer.setAttribute("expand-empty", "false");
|
||||
viewer.setAttribute("data", jsonText(value));
|
||||
container.textContent = "";
|
||||
container.appendChild(viewer);
|
||||
}
|
||||
|
||||
function cell(value: string, code: boolean): HTMLTableCellElement {
|
||||
const td = document.createElement("td");
|
||||
if (code) {
|
||||
const codeElement = document.createElement("code");
|
||||
codeElement.textContent = value;
|
||||
td.appendChild(codeElement);
|
||||
} else {
|
||||
td.textContent = value;
|
||||
}
|
||||
return td;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-app-demo-desktop/frontend/ts/main.ts
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
import * as bootstrap from "bootstrap";
|
||||
import MarkdownIt from "markdown-it";
|
||||
@@ -60,3 +60,19 @@ if (openDemoBackfillLink) {
|
||||
void invoke("open_demo_backfill_window");
|
||||
});
|
||||
}
|
||||
|
||||
const sqlDemoLinks: ReadonlyArray<readonly [string, string]> = [
|
||||
["openDemoSqlDiagLink", "open_demo_sql_diag_window"],
|
||||
["openDemoSqlPgRawLink", "open_demo_sql_pg_raw_window"],
|
||||
["openDemoSqlPgCoreLink", "open_demo_sql_pg_core_window"],
|
||||
["openDemoSqlReplayCandidatesLink", "open_demo_sql_replay_candidates_window"],
|
||||
];
|
||||
for (const [elementId, command] of sqlDemoLinks) {
|
||||
const link = document.querySelector<HTMLAnchorElement>(`#${elementId}`);
|
||||
if (link) {
|
||||
link.addEventListener("click", event => {
|
||||
event.preventDefault();
|
||||
void invoke(command);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user