v0.5.3-pre.005-fix010
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<!-- file: kb-app-demo-desktop/frontend/demo_decode_replay.html -->
|
||||
<!-- version: 9 -->
|
||||
<!-- version: 10 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
@@ -131,11 +131,13 @@
|
||||
<label class="form-label" for="annotationSignatureInput">Journal des annotations — signature contient</label>
|
||||
<input id="annotationSignatureInput" class="form-control font-monospace" placeholder="Filtre facultatif" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label" for="annotationLimitInput">Limite</label>
|
||||
<input id="annotationLimitInput" class="form-control" type="number" min="1" max="500" value="100" />
|
||||
</div>
|
||||
<button id="loadAnnotationsButton" class="btn btn-outline-primary" type="button">Charger les annotations</button>
|
||||
<div class="btn-group" role="group" aria-label="Navigation des blocs Store d’annotations">
|
||||
<button id="annotationFirstBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Premier bloc</button>
|
||||
<button id="annotationPreviousBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Bloc précédent</button>
|
||||
<button id="annotationNextBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Bloc suivant</button>
|
||||
</div>
|
||||
<span id="annotationBlockSummary" class="badge text-bg-light border">Bloc — / —</span>
|
||||
</div>
|
||||
<p id="annotationJournalStatus" class="text-body-secondary mb-3">Annotations non chargées.</p>
|
||||
<div class="table-responsive">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: kb-app-demo-desktop/frontend/demo_execution_spl.html -->
|
||||
<!-- version: 15 -->
|
||||
<!-- version: 16 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
@@ -306,7 +306,12 @@
|
||||
<h2 class="h5 mb-0">Journal métier SPL Token</h2>
|
||||
<div class="form-text">Événements commités uniquement, sans solde final reconstruit ni OHLC.</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-primary" id="refreshTokenJournalButton" type="button">Actualiser</button>
|
||||
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||
<button class="btn btn-sm btn-outline-primary" id="refreshTokenJournalButton" type="button">Charger</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" data-store-block-navigation="true" disabled="" id="tokenJournalFirstBlockButton" type="button">Premier bloc</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" data-store-block-navigation="true" disabled="" id="tokenJournalPreviousBlockButton" type="button">Bloc précédent</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" data-store-block-navigation="true" disabled="" id="tokenJournalNextBlockButton" type="button">Bloc suivant</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-12 col-lg-6">
|
||||
@@ -330,15 +335,15 @@
|
||||
<option value="risk">risk</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6 col-lg-3">
|
||||
<label class="form-label" for="tokenJournalLimitInput">Limite</label>
|
||||
<input class="form-control" id="tokenJournalLimitInput" max="500" min="1" step="1" type="number" value="100" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="tokenJournalOperationInput">Opération exacte</label>
|
||||
<input class="form-control font-monospace" id="tokenJournalOperationInput" placeholder="transfer_checked" type="text" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
|
||||
<span class="badge text-bg-light border" id="tokenJournalBlockSummary">Bloc 0 / 0 · 0 résultat · 500/bloc</span>
|
||||
<span class="form-text">Le total de blocs est recalculé à chaque chargement ou navigation.</span>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover align-middle">
|
||||
<thead>
|
||||
@@ -375,7 +380,12 @@
|
||||
<h2 class="h5 mb-0">Journal lifecycle ATA</h2>
|
||||
<div class="form-text">Création, réutilisation idempotente et récupération nested, sans duplication des CPI Token.</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-primary" id="refreshAtaJournalButton" type="button">Actualiser</button>
|
||||
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||
<button class="btn btn-sm btn-outline-primary" id="refreshAtaJournalButton" type="button">Charger</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" data-store-block-navigation="true" disabled="" id="ataJournalFirstBlockButton" type="button">Premier bloc</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" data-store-block-navigation="true" disabled="" id="ataJournalPreviousBlockButton" type="button">Bloc précédent</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" data-store-block-navigation="true" disabled="" id="ataJournalNextBlockButton" type="button">Bloc suivant</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-12 col-lg-6">
|
||||
@@ -390,14 +400,14 @@
|
||||
<label class="form-label" for="ataJournalAddressInput">ATA exact</label>
|
||||
<input class="form-control font-monospace" id="ataJournalAddressInput" type="text" />
|
||||
</div>
|
||||
<div class="col-8 col-lg-4">
|
||||
<div class="col-12 col-lg-6">
|
||||
<label class="form-label" for="ataJournalOperationInput">Opération</label>
|
||||
<input class="form-control font-monospace" id="ataJournalOperationInput" type="text" />
|
||||
</div>
|
||||
<div class="col-4 col-lg-2">
|
||||
<label class="form-label" for="ataJournalLimitInput">Limite</label>
|
||||
<input class="form-control" id="ataJournalLimitInput" max="500" min="1" type="number" value="100" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
|
||||
<span class="badge text-bg-light border" id="ataJournalBlockSummary">Bloc 0 / 0 · 0 résultat · 500/bloc</span>
|
||||
<span class="form-text">Le total de blocs est recalculé à chaque chargement ou navigation.</span>
|
||||
</div>
|
||||
<textarea class="form-control font-monospace" data-json-viewer="true" id="ataJournalOutput" readonly="" rows="14">{"title":"Journal lifecycle ATA","status":"idle","message":"Aucune lecture."}</textarea>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: kb-app-demo-desktop/frontend/demo_store_core.html -->
|
||||
<!-- version: 8 -->
|
||||
<!-- version: 9 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap gap-2 justify-content-between align-items-center mb-3">
|
||||
<h1 class="h3 card-title mb-0">Core store Solana</h1>
|
||||
<button id="refreshSqlCoreButton" class="btn btn-primary" type="button">Rafraîchir</button>
|
||||
<button id="refreshStoreCoreButton" class="btn btn-primary" type="button">Rafraîchir</button>
|
||||
</div>
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-sm-3">Statut</dt>
|
||||
@@ -94,4 +94,4 @@
|
||||
<script type="module" src="ts/demo_store_core.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: kb-app-demo-desktop/frontend/demo_store_diag.html -->
|
||||
<!-- version: 7 -->
|
||||
<!-- version: 8 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
<div class="d-flex flex-wrap gap-2 justify-content-between align-items-center mb-3">
|
||||
<h1 class="h3 card-title mb-0">Diagnostic du store</h1>
|
||||
<div class="d-flex gap-2">
|
||||
<button id="refreshSqlDiagButton" class="btn btn-primary" type="button">Rafraîchir</button>
|
||||
<button id="copySqlDiagButton" class="btn btn-outline-secondary" type="button">Copier JSON</button>
|
||||
<button id="refreshStoreDiagButton" class="btn btn-primary" type="button">Rafraîchir</button>
|
||||
<button id="copyStoreDiagButton" class="btn btn-outline-secondary" type="button">Copier JSON</button>
|
||||
</div>
|
||||
</div>
|
||||
<dl class="row mb-0">
|
||||
@@ -107,4 +107,4 @@
|
||||
<script type="module" src="ts/demo_store_diag.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: kb-app-demo-desktop/frontend/demo_store_raw.html -->
|
||||
<!-- version: 9 -->
|
||||
<!-- version: 10 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-wrap gap-2 justify-content-between align-items-center mb-3">
|
||||
<h1 class="h3 card-title mb-0">Acquisition transactionnelle Solana</h1>
|
||||
<button id="refreshSqlRawButton" class="btn btn-primary" type="button">Rafraîchir</button>
|
||||
<button id="refreshStoreRawButton" class="btn btn-primary" type="button">Rafraîchir</button>
|
||||
</div>
|
||||
<dl class="row mb-0">
|
||||
<dt class="col-sm-3">Statut</dt>
|
||||
@@ -94,4 +94,4 @@
|
||||
<script type="module" src="ts/demo_store_raw.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: kb-app-demo-desktop/frontend/demo_store_replay_candidates.html -->
|
||||
<!-- version: 17 -->
|
||||
<!-- version: 18 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
@@ -36,12 +36,12 @@
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<h1 class="h3 card-title">Exploration read-only des candidats</h1>
|
||||
<p class="text-body-secondary mb-2">Les requêtes sont paramétrées, bornées et indépendantes du backend concret. Utilisez les cases à cocher pour sélectionner les lignes. La case du header sélectionne ou désélectionne toutes les lignes correspondant au filtre local. Sans sélection explicite, la copie et l’export CSV utilisent toutes les lignes filtrées. Les CSV UTF-8 avec BOM et séparateur point-virgule sont écrits par le backend Tauri dans <code>./data/exports_csv/</code>. LibreOffice Calc peut toujours afficher son dialogue d’import, car le format CSV ne décrit pas formellement son encodage ni son séparateur.</p>
|
||||
<p class="text-body-secondary mb-2">Les requêtes sont paramétrées, bornées et indépendantes du backend concret. La navigation <strong>Premier bloc / Bloc précédent / Bloc suivant</strong> recharge le Store par blocs fixes de 500 et recalcule leur nombre total à chaque chargement ; la pagination, le tri et le filtre DataTables restent locaux au bloc déjà chargé. Utilisez les cases à cocher pour sélectionner les lignes. La case du header sélectionne ou désélectionne toutes les lignes correspondant au filtre local. Sans sélection explicite, la copie et l’export CSV utilisent toutes les lignes filtrées du bloc courant. Les CSV UTF-8 avec BOM et séparateur point-virgule sont écrits par le backend Tauri dans <code>./data/exports_csv/</code>. LibreOffice Calc peut toujours afficher son dialogue d’import, car le format CSV ne décrit pas formellement son encodage ni son séparateur.</p>
|
||||
<div class="small text-body-secondary">
|
||||
<span class="fw-semibold">Connexion :</span>
|
||||
<code id="replayCandidateDsn">—</code>
|
||||
<span class="ms-3 fw-semibold">Limite serveur maximale :</span>
|
||||
<code id="replayCandidateMaximumLimit">—</code>
|
||||
<span class="ms-3 fw-semibold">Taille fixe d’un bloc Store :</span>
|
||||
<code id="replayCandidateBlockSize">—</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -129,10 +129,6 @@
|
||||
<label for="transactionEntityValueInput" class="form-label">Entité exacte</label>
|
||||
<input id="transactionEntityValueInput" class="form-control font-monospace" type="text" autocomplete="off" />
|
||||
</div>
|
||||
<div class="col-6 col-md-3 col-xl-1">
|
||||
<label for="transactionLimitInput" class="form-label">Limite</label>
|
||||
<input id="transactionLimitInput" class="form-control" type="number" min="1" max="500" step="1" value="500" />
|
||||
</div>
|
||||
<div class="col-6 col-md-3 col-xl-2">
|
||||
<div class="form-check form-switch mb-2">
|
||||
<input id="transactionNewestFirstInput" class="form-check-input" type="checkbox" role="switch" checked />
|
||||
@@ -155,6 +151,14 @@
|
||||
<div>
|
||||
<h2 class="h5 card-title mb-1">Transactions candidates</h2>
|
||||
<div id="transactionResultSummary" class="small text-body-secondary">Aucune requête exécutée.</div>
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 mt-2" aria-label="Pagination Store transaction">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button id="transactionFirstBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Premier bloc</button>
|
||||
<button id="transactionPreviousBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Bloc précédent</button>
|
||||
<button id="transactionNextBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Bloc suivant</button>
|
||||
</div>
|
||||
<span id="transactionBlockSummary" class="badge text-bg-light border">Bloc — / —</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="btn-group flex-wrap" role="group" aria-label="Actions signatures">
|
||||
@@ -199,15 +203,11 @@
|
||||
<option value="">Tous les programmes connus</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="col-12 col-lg-6">
|
||||
<label for="programContainsInput" class="form-label">Program ID contient</label>
|
||||
<input id="programContainsInput" class="form-control font-monospace" type="text" list="knownProgramDatalist" autocomplete="off" />
|
||||
<datalist id="knownProgramDatalist"></datalist>
|
||||
</div>
|
||||
<div class="col-6 col-lg-1">
|
||||
<label for="programLimitInput" class="form-label">Limite</label>
|
||||
<input id="programLimitInput" class="form-control" type="number" min="1" max="500" step="1" value="500" />
|
||||
</div>
|
||||
<div class="col-12 col-lg-2">
|
||||
<div class="d-grid gap-2">
|
||||
<button id="resetProgramsButton" class="btn btn-outline-secondary" type="button">Réinitialiser</button>
|
||||
@@ -223,6 +223,14 @@
|
||||
<div>
|
||||
<h2 class="h5 card-title mb-1">Programmes indexés</h2>
|
||||
<div id="programResultSummary" class="small text-body-secondary">Aucune requête exécutée.</div>
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 mt-2" aria-label="Pagination Store program">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button id="programFirstBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Premier bloc</button>
|
||||
<button id="programPreviousBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Bloc précédent</button>
|
||||
<button id="programNextBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Bloc suivant</button>
|
||||
</div>
|
||||
<span id="programBlockSummary" class="badge text-bg-light border">Bloc — / —</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="btn-group flex-wrap" role="group" aria-label="Actions programmes">
|
||||
@@ -258,14 +266,10 @@
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-12 col-lg-7">
|
||||
<div class="col-12 col-lg-8">
|
||||
<label for="mintContainsInput" class="form-label">Mint contient</label>
|
||||
<input id="mintContainsInput" class="form-control font-monospace" type="text" autocomplete="off" />
|
||||
</div>
|
||||
<div class="col-6 col-lg-2">
|
||||
<label for="mintLimitInput" class="form-label">Limite</label>
|
||||
<input id="mintLimitInput" class="form-control" type="number" min="1" max="500" step="1" value="500" />
|
||||
</div>
|
||||
<div class="col-12 col-lg-3">
|
||||
<div class="d-grid d-sm-flex gap-2">
|
||||
<button id="resetMintsButton" class="btn btn-outline-secondary" type="button">Réinitialiser</button>
|
||||
@@ -281,6 +285,14 @@
|
||||
<div>
|
||||
<h2 class="h5 card-title mb-1">Mints</h2>
|
||||
<div id="mintResultSummary" class="small text-body-secondary">Aucune requête exécutée.</div>
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 mt-2" aria-label="Pagination Store mint">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button id="mintFirstBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Premier bloc</button>
|
||||
<button id="mintPreviousBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Bloc précédent</button>
|
||||
<button id="mintNextBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Bloc suivant</button>
|
||||
</div>
|
||||
<span id="mintBlockSummary" class="badge text-bg-light border">Bloc — / —</span>
|
||||
</div>
|
||||
<div class="small text-body-secondary">Source : faits Core de variation de balance (mint).</div>
|
||||
</div>
|
||||
<div>
|
||||
@@ -316,14 +328,10 @@
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-12 col-lg-7">
|
||||
<div class="col-12 col-lg-8">
|
||||
<label for="ownerContainsInput" class="form-label">Owner contient</label>
|
||||
<input id="ownerContainsInput" class="form-control font-monospace" type="text" autocomplete="off" />
|
||||
</div>
|
||||
<div class="col-6 col-lg-2">
|
||||
<label for="ownerLimitInput" class="form-label">Limite</label>
|
||||
<input id="ownerLimitInput" class="form-control" type="number" min="1" max="500" step="1" value="500" />
|
||||
</div>
|
||||
<div class="col-12 col-lg-3">
|
||||
<div class="d-grid d-sm-flex gap-2">
|
||||
<button id="resetOwnersButton" class="btn btn-outline-secondary" type="button">Réinitialiser</button>
|
||||
@@ -339,6 +347,14 @@
|
||||
<div>
|
||||
<h2 class="h5 card-title mb-1">Owners</h2>
|
||||
<div id="ownerResultSummary" class="small text-body-secondary">Aucune requête exécutée.</div>
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 mt-2" aria-label="Pagination Store owner">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button id="ownerFirstBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Premier bloc</button>
|
||||
<button id="ownerPreviousBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Bloc précédent</button>
|
||||
<button id="ownerNextBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Bloc suivant</button>
|
||||
</div>
|
||||
<span id="ownerBlockSummary" class="badge text-bg-light border">Bloc — / —</span>
|
||||
</div>
|
||||
<div class="small text-body-secondary">Source : faits Core de variation de balance (owner).</div>
|
||||
</div>
|
||||
<div>
|
||||
@@ -374,14 +390,10 @@
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-12 col-lg-7">
|
||||
<div class="col-12 col-lg-8">
|
||||
<label for="accountContainsInput" class="form-label">Account key contient</label>
|
||||
<input id="accountContainsInput" class="form-control font-monospace" type="text" autocomplete="off" />
|
||||
</div>
|
||||
<div class="col-6 col-lg-2">
|
||||
<label for="accountLimitInput" class="form-label">Limite</label>
|
||||
<input id="accountLimitInput" class="form-control" type="number" min="1" max="500" step="1" value="500" />
|
||||
</div>
|
||||
<div class="col-12 col-lg-3">
|
||||
<div class="d-grid d-sm-flex gap-2">
|
||||
<button id="resetAccountsButton" class="btn btn-outline-secondary" type="button">Réinitialiser</button>
|
||||
@@ -397,6 +409,14 @@
|
||||
<div>
|
||||
<h2 class="h5 card-title mb-1">Comptes</h2>
|
||||
<div id="accountResultSummary" class="small text-body-secondary">Aucune requête exécutée.</div>
|
||||
<div class="d-flex flex-wrap align-items-center gap-2 mt-2" aria-label="Pagination Store account">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button id="accountFirstBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Premier bloc</button>
|
||||
<button id="accountPreviousBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Bloc précédent</button>
|
||||
<button id="accountNextBlockButton" class="btn btn-outline-secondary" type="button" data-store-block-navigation="true" disabled>Bloc suivant</button>
|
||||
</div>
|
||||
<span id="accountBlockSummary" class="badge text-bg-light border">Bloc — / —</span>
|
||||
</div>
|
||||
<div class="small text-body-secondary">Source : clés de comptes Core résolues.</div>
|
||||
</div>
|
||||
<div>
|
||||
@@ -444,4 +464,4 @@
|
||||
<script type="module" src="ts/demo_store_replay_candidates.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-app-demo-desktop/frontend/ts/demo_decode_replay.ts
|
||||
// version: 8
|
||||
// version: 9
|
||||
|
||||
import * as bootstrap from "bootstrap";
|
||||
import "simplebar";
|
||||
@@ -9,11 +9,21 @@ import DataTable, { type Api } from "datatables.net-bs5";
|
||||
import "datatables.net-bs5/css/dataTables.bootstrap5.css";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log.ts";
|
||||
import {
|
||||
applyStoreBlockMetadata,
|
||||
createStoreBlockState,
|
||||
cursorForBlock,
|
||||
markStoreBlockFiltersDirty,
|
||||
resetStoreBlockState,
|
||||
renderStoreBlockControls,
|
||||
type StoreBlockControls,
|
||||
} from "./demo_store_block_pagination.ts";
|
||||
import type { DemoDecodeDiagnosticsPayload } from "./bindings/kb_app_demo_desktop/demo_decode_replay/DemoDecodeDiagnosticsPayload.ts";
|
||||
import type { DemoDecodeReplayOptionsPayload } from "./bindings/kb_app_demo_desktop/demo_decode_replay/DemoDecodeReplayOptionsPayload.ts";
|
||||
import type { DemoDecodeReplayProgressPayload } from "./bindings/kb_app_demo_desktop/demo_decode_replay/DemoDecodeReplayProgressPayload.ts";
|
||||
import type { DemoDecodeReplayRequest } from "./bindings/kb_app_demo_desktop/demo_decode_replay/DemoDecodeReplayRequest.ts";
|
||||
import type { DemoDecodeReplaySummaryPayload } from "./bindings/kb_app_demo_desktop/demo_decode_replay/DemoDecodeReplaySummaryPayload.ts";
|
||||
import type { DemoTransactionAnnotationPagePayload } from "./bindings/kb_app_demo_desktop/demo_decode_replay/DemoTransactionAnnotationPagePayload.ts";
|
||||
import type { DemoTransactionAnnotationRequest } from "./bindings/kb_app_demo_desktop/demo_decode_replay/DemoTransactionAnnotationRequest.ts";
|
||||
import type { DemoTransactionAnnotationRow } from "./bindings/kb_app_demo_desktop/demo_decode_replay/DemoTransactionAnnotationRow.ts";
|
||||
|
||||
@@ -23,6 +33,15 @@ import type { DemoTransactionAnnotationRow } from "./bindings/kb_app_demo_deskto
|
||||
const logLines: string[] = [];
|
||||
let running = false;
|
||||
let materializationAvailable = false;
|
||||
const annotationBlockControls: StoreBlockControls = {
|
||||
firstSelector: "#annotationFirstBlockButton",
|
||||
previousSelector: "#annotationPreviousBlockButton",
|
||||
nextSelector: "#annotationNextBlockButton",
|
||||
summarySelector: "#annotationBlockSummary",
|
||||
};
|
||||
const annotationBlockState = createStoreBlockState();
|
||||
type AnnotationBaseRequest = Omit<DemoTransactionAnnotationRequest, "cursor" | "blockIndex">;
|
||||
let annotationBaseRequest: AnnotationBaseRequest | null = null;
|
||||
|
||||
function element<T extends HTMLElement>(selector: string): T {
|
||||
const value = document.querySelector<T>(selector);
|
||||
@@ -250,7 +269,7 @@ function renderAnnotations(rows: DemoTransactionAnnotationRow[]): void {
|
||||
annotationJournalTable = new DataTable<DemoTransactionAnnotationRow>("#annotationJournalTable", {
|
||||
autoWidth: false,
|
||||
pageLength: 25,
|
||||
lengthMenu: [10, 25, 50, 100],
|
||||
lengthMenu: [10, 25, 50, 100, 250, 500],
|
||||
order: [[0, "desc"]],
|
||||
scrollX: true,
|
||||
language: {
|
||||
@@ -265,23 +284,53 @@ function renderAnnotations(rows: DemoTransactionAnnotationRow[]): void {
|
||||
element<HTMLElement>("#annotationJournalStatus").textContent = `${rows.length} annotation(s) committed chargée(s).`;
|
||||
}
|
||||
|
||||
async function loadAnnotations(): Promise<void> {
|
||||
const request: DemoTransactionAnnotationRequest = {
|
||||
async function startAnnotationLoad(): Promise<void> {
|
||||
annotationBaseRequest = {
|
||||
signatureContains: inputValue("#annotationSignatureInput") || null,
|
||||
limit: integerValue("#annotationLimitInput"),
|
||||
};
|
||||
resetStoreBlockState(annotationBlockState);
|
||||
await loadAnnotationBlock(1, null);
|
||||
}
|
||||
|
||||
async function loadAnnotationBlock(blockIndex: number, cursor: string | null): Promise<void> {
|
||||
if (annotationBaseRequest === null) {
|
||||
throw new Error("Rechargez les annotations pour initialiser les filtres Store.");
|
||||
}
|
||||
const request: DemoTransactionAnnotationRequest = {
|
||||
...annotationBaseRequest,
|
||||
cursor,
|
||||
blockIndex,
|
||||
};
|
||||
try {
|
||||
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke demo_decode_replay_annotations limit=${request.limit}`);
|
||||
const rows = await invoke<DemoTransactionAnnotationRow[]>("demo_decode_replay_annotations", { request });
|
||||
renderAnnotations(rows);
|
||||
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke demo_decode_replay_annotations completed rows=${rows.length}`);
|
||||
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke demo_decode_replay_annotations block=${blockIndex}`);
|
||||
const payload = await invoke<DemoTransactionAnnotationPagePayload>("demo_decode_replay_annotations", { request });
|
||||
renderAnnotations(payload.rows);
|
||||
applyStoreBlockMetadata(annotationBlockState, payload.page, annotationBlockControls);
|
||||
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke demo_decode_replay_annotations completed block=${payload.page.blockIndex}/${payload.page.totalBlocks} rows=${payload.rows.length}`);
|
||||
} catch (caughtError) {
|
||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||
element<HTMLElement>("#annotationJournalStatus").textContent = `Erreur : ${message}`;
|
||||
renderStoreBlockControls(annotationBlockState, annotationBlockControls);
|
||||
frontendError("kb_app_demo_desktop.frontend.demo_decode_replay", `Annotation journal loading failed: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPreviousAnnotationBlock(): Promise<void> {
|
||||
const targetBlock = annotationBlockState.currentBlock - 1;
|
||||
const cursor = cursorForBlock(annotationBlockState, targetBlock);
|
||||
if (targetBlock < 1 || cursor === undefined) {
|
||||
return;
|
||||
}
|
||||
await loadAnnotationBlock(targetBlock, cursor);
|
||||
}
|
||||
|
||||
async function loadNextAnnotationBlock(): Promise<void> {
|
||||
if (annotationBlockState.nextCursor === null) {
|
||||
return;
|
||||
}
|
||||
await loadAnnotationBlock(annotationBlockState.currentBlock + 1, annotationBlockState.nextCursor);
|
||||
}
|
||||
|
||||
async function openWindow(command: "open_demo_store_core_window" | "open_demo_store_replay_candidates_window"): Promise<void> {
|
||||
try {
|
||||
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke ${command}`);
|
||||
@@ -357,7 +406,14 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
element<HTMLButtonElement>("#startDecodeReplayButton").addEventListener("click", () => void executeDecodeReplay());
|
||||
element<HTMLButtonElement>("#cancelDecodeReplayButton").addEventListener("click", () => void cancelDecodeReplay());
|
||||
element<HTMLButtonElement>("#loadDecodeDiagnosticsButton").addEventListener("click", () => void loadDiagnostics());
|
||||
element<HTMLButtonElement>("#loadAnnotationsButton").addEventListener("click", () => void loadAnnotations());
|
||||
element<HTMLButtonElement>("#loadAnnotationsButton").addEventListener("click", () => void startAnnotationLoad());
|
||||
element<HTMLButtonElement>(annotationBlockControls.firstSelector).addEventListener("click", () => void loadAnnotationBlock(1, null));
|
||||
element<HTMLButtonElement>(annotationBlockControls.previousSelector).addEventListener("click", () => void loadPreviousAnnotationBlock());
|
||||
element<HTMLButtonElement>(annotationBlockControls.nextSelector).addEventListener("click", () => void loadNextAnnotationBlock());
|
||||
element<HTMLInputElement>("#annotationSignatureInput").addEventListener("input", () => {
|
||||
annotationBaseRequest = null;
|
||||
markStoreBlockFiltersDirty(annotationBlockState, annotationBlockControls);
|
||||
});
|
||||
element<HTMLButtonElement>("#openReplayCandidatesFromDecodeButton").addEventListener("click", () => void openWindow("open_demo_store_replay_candidates_window"));
|
||||
element<HTMLButtonElement>("#openCoreDiagnosticsFromDecodeButton").addEventListener("click", () => void openWindow("open_demo_store_core_window"));
|
||||
void listen<DemoDecodeReplayProgressPayload>("demo-decode-replay-progress", event => appendLog(event.payload));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-app-demo-desktop/frontend/ts/demo_execution_metadata.ts
|
||||
// version: 14
|
||||
// version: 15
|
||||
|
||||
import * as bootstrap from "bootstrap";
|
||||
import "simplebar";
|
||||
@@ -479,7 +479,9 @@ async function prepareProfile(): Promise<void> {
|
||||
setExecutionRunning(executionRunning);
|
||||
updateProfileStatus("Préparation", "badge text-bg-warning");
|
||||
try {
|
||||
frontendDebug("kb-app-demo-desktop.frontend.execution_metadata", `Invoke demo_execution_devnet_prepare_profile profile=${profileName}`);
|
||||
const readiness = await invoke<DemoExecutionDevnetStoreReadinessPayload>("demo_execution_devnet_prepare_profile", { profileName });
|
||||
frontendDebug("kb-app-demo-desktop.frontend.execution_metadata", `Invoke demo_execution_devnet_prepare_profile completed profile=${profileName} resources=${readiness.availableResourcesAfter}/${readiness.expectedResources}`);
|
||||
synchronizeProfileSelections(profileName);
|
||||
window.localStorage.setItem(profileStorageKey, profileName);
|
||||
for (const selector of profileOutputSelectors) {
|
||||
@@ -580,9 +582,18 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
void prepareProfile();
|
||||
});
|
||||
}
|
||||
element<HTMLButtonElement>("#prepareMetadataProfileButton").addEventListener("click", () => void prepareProfile());
|
||||
element<HTMLButtonElement>("#prepareToken2022MetadataProfileButton").addEventListener("click", () => void prepareProfile());
|
||||
element<HTMLButtonElement>("#prepareSolanaProgramMetadataProfileButton").addEventListener("click", () => void prepareProfile());
|
||||
element<HTMLButtonElement>("#prepareMetadataProfileButton").addEventListener("click", () => {
|
||||
frontendDebug("kb-app-demo-desktop.frontend.execution_metadata", "Metaplex profile load button clicked");
|
||||
void prepareProfile();
|
||||
});
|
||||
element<HTMLButtonElement>("#prepareToken2022MetadataProfileButton").addEventListener("click", () => {
|
||||
frontendDebug("kb-app-demo-desktop.frontend.execution_metadata", "Token-2022 Metadata profile load button clicked");
|
||||
void prepareProfile();
|
||||
});
|
||||
element<HTMLButtonElement>("#prepareSolanaProgramMetadataProfileButton").addEventListener("click", () => {
|
||||
frontendDebug("kb-app-demo-desktop.frontend.execution_metadata", "Solana Program Metadata profile load button clicked");
|
||||
void prepareProfile();
|
||||
});
|
||||
element<HTMLButtonElement>("#refreshMetadataScenariosButton").addEventListener("click", () => void loadScenarios());
|
||||
element<HTMLTextAreaElement>("#metadataExecutionLogOutput").addEventListener("app-log-clear", () => { metadataLogLines.length = 0; });
|
||||
void listen<DemoExecutionMetadataProgressPayload>("demo-execution-metadata-progress", event => appendMetadataLog(event.payload));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-app-demo-desktop/frontend/ts/demo_execution_spl.ts
|
||||
// version: 11
|
||||
// version: 13
|
||||
|
||||
import * as bootstrap from "bootstrap";
|
||||
import "simplebar";
|
||||
@@ -19,13 +19,23 @@ import type { DemoExecutionMemoSummaryPayload } from "./bindings/kb_app_demo_des
|
||||
import type { DemoExecutionSplTokenRequest } from "./bindings/kb_app_demo_desktop/demo_spl_token/DemoExecutionSplTokenRequest.ts";
|
||||
import type { DemoExecutionSplTokenSummaryPayload } from "./bindings/kb_app_demo_desktop/demo_spl_token/DemoExecutionSplTokenSummaryPayload.ts";
|
||||
import type { DemoSplTokenJournalRequest } from "./bindings/kb_app_demo_desktop/demo_spl_token/DemoSplTokenJournalRequest.ts";
|
||||
import type { DemoSplTokenJournalPagePayload } from "./bindings/kb_app_demo_desktop/demo_spl_token/DemoSplTokenJournalPagePayload.ts";
|
||||
import type { DemoSplTokenJournalRow } from "./bindings/kb_app_demo_desktop/demo_spl_token/DemoSplTokenJournalRow.ts";
|
||||
import type { DemoExecutionSplAtaRequest } from "./bindings/kb_app_demo_desktop/demo_spl_ata/DemoExecutionSplAtaRequest.ts";
|
||||
import type { DemoExecutionSplAtaSummaryPayload } from "./bindings/kb_app_demo_desktop/demo_spl_ata/DemoExecutionSplAtaSummaryPayload.ts";
|
||||
import type { DemoSplAtaDerivationPayload } from "./bindings/kb_app_demo_desktop/demo_spl_ata/DemoSplAtaDerivationPayload.ts";
|
||||
import type { DemoSplAtaDerivationRequest } from "./bindings/kb_app_demo_desktop/demo_spl_ata/DemoSplAtaDerivationRequest.ts";
|
||||
import type { DemoSplAtaJournalRequest } from "./bindings/kb_app_demo_desktop/demo_spl_ata/DemoSplAtaJournalRequest.ts";
|
||||
import type { DemoSplAtaJournalRow } from "./bindings/kb_app_demo_desktop/demo_spl_ata/DemoSplAtaJournalRow.ts";
|
||||
import type { DemoSplAtaJournalPagePayload } from "./bindings/kb_app_demo_desktop/demo_spl_ata/DemoSplAtaJournalPagePayload.ts";
|
||||
import {
|
||||
applyStoreBlockMetadata,
|
||||
createStoreBlockState,
|
||||
cursorForBlock,
|
||||
markStoreBlockFiltersDirty,
|
||||
renderStoreBlockControls,
|
||||
resetStoreBlockState,
|
||||
type StoreBlockControls,
|
||||
} from "./demo_store_block_pagination";
|
||||
import type { DemoExecutionSplToken2022Request } from "./bindings/kb_app_demo_desktop/demo_spl_token_2022/DemoExecutionSplToken2022Request.ts";
|
||||
import type { DemoExecutionSplToken2022SummaryPayload } from "./bindings/kb_app_demo_desktop/demo_spl_token_2022/DemoExecutionSplToken2022SummaryPayload.ts";
|
||||
import type { DemoSplToken2022FixturePayload } from "./bindings/kb_app_demo_desktop/demo_spl_token_2022/DemoSplToken2022FixturePayload.ts";
|
||||
@@ -40,6 +50,24 @@ let running = false;
|
||||
let profileStoreReady = false;
|
||||
let profileOptions: DemoExecutionSolanaCoreProfileOption[] = [];
|
||||
let token_2022Fixture: DemoSplToken2022FixturePayload | null = null;
|
||||
const tokenJournalBlockState = createStoreBlockState();
|
||||
const ataJournalBlockState = createStoreBlockState();
|
||||
const tokenJournalBlockControls: StoreBlockControls = {
|
||||
firstSelector: "#tokenJournalFirstBlockButton",
|
||||
previousSelector: "#tokenJournalPreviousBlockButton",
|
||||
nextSelector: "#tokenJournalNextBlockButton",
|
||||
summarySelector: "#tokenJournalBlockSummary",
|
||||
};
|
||||
const ataJournalBlockControls: StoreBlockControls = {
|
||||
firstSelector: "#ataJournalFirstBlockButton",
|
||||
previousSelector: "#ataJournalPreviousBlockButton",
|
||||
nextSelector: "#ataJournalNextBlockButton",
|
||||
summarySelector: "#ataJournalBlockSummary",
|
||||
};
|
||||
type TokenJournalBaseRequest = Omit<DemoSplTokenJournalRequest, "cursor" | "blockIndex">;
|
||||
type AtaJournalBaseRequest = Omit<DemoSplAtaJournalRequest, "cursor" | "blockIndex">;
|
||||
let tokenJournalBaseRequest: TokenJournalBaseRequest | null = null;
|
||||
let ataJournalBaseRequest: AtaJournalBaseRequest | null = null;
|
||||
|
||||
function element<T extends HTMLElement>(selector: string): T {
|
||||
const value = document.querySelector<T>(selector);
|
||||
@@ -400,7 +428,7 @@ async function executeAta(submit: boolean): Promise<void> {
|
||||
element<HTMLInputElement>("#ataJournalSignatureInput").value = summary.transactionSignature;
|
||||
element<HTMLInputElement>("#ataJournalMintInput").value = request.mint;
|
||||
element<HTMLInputElement>("#ataJournalAddressInput").value = summary.associatedTokenAccount;
|
||||
await loadAtaJournal();
|
||||
await startAtaJournalLoad();
|
||||
}
|
||||
} catch (caughtError) {
|
||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||
@@ -412,27 +440,52 @@ async function executeAta(submit: boolean): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function ataJournalRequest(): DemoSplAtaJournalRequest {
|
||||
function ataJournalBaseFilters(): AtaJournalBaseRequest {
|
||||
return {
|
||||
profileName: element<HTMLSelectElement>("#executionProfileSelect").value,
|
||||
signatureContains: optionalText("#ataJournalSignatureInput"),
|
||||
mint: optionalText("#ataJournalMintInput"),
|
||||
associatedTokenAccount: optionalText("#ataJournalAddressInput"),
|
||||
operation: optionalText("#ataJournalOperationInput"),
|
||||
limit: integerValue("#ataJournalLimitInput"),
|
||||
};
|
||||
}
|
||||
|
||||
async function loadAtaJournal(): Promise<void> {
|
||||
async function startAtaJournalLoad(): Promise<void> {
|
||||
ataJournalBaseRequest = ataJournalBaseFilters();
|
||||
resetStoreBlockState(ataJournalBlockState);
|
||||
await loadAtaJournalBlock(1);
|
||||
}
|
||||
|
||||
async function loadAtaJournalBlock(blockIndex: number): Promise<void> {
|
||||
if (ataJournalBaseRequest === null) {
|
||||
markStoreBlockFiltersDirty(ataJournalBlockState, ataJournalBlockControls);
|
||||
return;
|
||||
}
|
||||
const cursor = cursorForBlock(ataJournalBlockState, blockIndex);
|
||||
if (cursor === undefined) {
|
||||
return;
|
||||
}
|
||||
const button = element<HTMLButtonElement>("#refreshAtaJournalButton");
|
||||
button.disabled = true;
|
||||
element<HTMLButtonElement>(ataJournalBlockControls.firstSelector).disabled = true;
|
||||
element<HTMLButtonElement>(ataJournalBlockControls.previousSelector).disabled = true;
|
||||
element<HTMLButtonElement>(ataJournalBlockControls.nextSelector).disabled = true;
|
||||
const request: DemoSplAtaJournalRequest = {
|
||||
...ataJournalBaseRequest,
|
||||
cursor,
|
||||
blockIndex,
|
||||
};
|
||||
try {
|
||||
const rows = await invoke<DemoSplAtaJournalRow[]>("demo_spl_ata_journal", { request: ataJournalRequest() });
|
||||
frontendDebug("kb-app-demo-desktop.frontend.demo_execution_spl", `Invoke demo_spl_ata_journal block=${blockIndex}`);
|
||||
const payload = await invoke<DemoSplAtaJournalPagePayload>("demo_spl_ata_journal", { request });
|
||||
applyStoreBlockMetadata(ataJournalBlockState, payload.page, ataJournalBlockControls);
|
||||
frontendDebug("kb-app-demo-desktop.frontend.demo_execution_spl", `Invoke demo_spl_ata_journal completed block=${payload.page.blockIndex}/${payload.page.totalBlocks} rows=${payload.rows.length}`);
|
||||
element<HTMLTextAreaElement>("#ataJournalOutput").value = JSON.stringify({
|
||||
title: "Journal lifecycle ATA",
|
||||
status: "success",
|
||||
count: rows.length,
|
||||
rows: rows.map(row => ({
|
||||
block: payload.page,
|
||||
count: payload.rows.length,
|
||||
rows: payload.rows.map(row => ({
|
||||
...row,
|
||||
payloadJson: parsedJson(row.payloadJson),
|
||||
})),
|
||||
@@ -445,11 +498,17 @@ async function loadAtaJournal(): Promise<void> {
|
||||
message,
|
||||
error: message,
|
||||
}, null, 2);
|
||||
renderStoreBlockControls(ataJournalBlockState, ataJournalBlockControls);
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function markAtaJournalFiltersDirty(): void {
|
||||
ataJournalBaseRequest = null;
|
||||
markStoreBlockFiltersDirty(ataJournalBlockState, ataJournalBlockControls);
|
||||
}
|
||||
|
||||
async function execute(submit: boolean): Promise<void> {
|
||||
if (running) {
|
||||
return;
|
||||
@@ -567,7 +626,7 @@ async function executeToken(submit: boolean): Promise<void> {
|
||||
});
|
||||
if (summary.transactionSignature) {
|
||||
element<HTMLInputElement>("#tokenJournalSignatureInput").value = summary.transactionSignature;
|
||||
await loadTokenJournal();
|
||||
await startTokenJournalLoad();
|
||||
}
|
||||
} catch (caughtError) {
|
||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||
@@ -579,7 +638,7 @@ async function executeToken(submit: boolean): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function tokenJournalRequest(): DemoSplTokenJournalRequest {
|
||||
function tokenJournalBaseFilters(): TokenJournalBaseRequest {
|
||||
return {
|
||||
profileName: element<HTMLSelectElement>("#executionProfileSelect").value,
|
||||
signatureContains: optionalText("#tokenJournalSignatureInput"),
|
||||
@@ -587,7 +646,6 @@ function tokenJournalRequest(): DemoSplTokenJournalRequest {
|
||||
account: optionalText("#tokenJournalAccountInput"),
|
||||
family: element<HTMLSelectElement>("#tokenJournalFamilySelect").value || null,
|
||||
operation: optionalText("#tokenJournalOperationInput"),
|
||||
limit: integerValue("#tokenJournalLimitInput"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -831,17 +889,42 @@ async function executeToken2022(submit: boolean): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTokenJournal(): Promise<void> {
|
||||
async function startTokenJournalLoad(): Promise<void> {
|
||||
tokenJournalBaseRequest = tokenJournalBaseFilters();
|
||||
resetStoreBlockState(tokenJournalBlockState);
|
||||
await loadTokenJournalBlock(1);
|
||||
}
|
||||
|
||||
async function loadTokenJournalBlock(blockIndex: number): Promise<void> {
|
||||
if (tokenJournalBaseRequest === null) {
|
||||
markStoreBlockFiltersDirty(tokenJournalBlockState, tokenJournalBlockControls);
|
||||
return;
|
||||
}
|
||||
const cursor = cursorForBlock(tokenJournalBlockState, blockIndex);
|
||||
if (cursor === undefined) {
|
||||
return;
|
||||
}
|
||||
const button = element<HTMLButtonElement>("#refreshTokenJournalButton");
|
||||
button.disabled = true;
|
||||
element<HTMLButtonElement>(tokenJournalBlockControls.firstSelector).disabled = true;
|
||||
element<HTMLButtonElement>(tokenJournalBlockControls.previousSelector).disabled = true;
|
||||
element<HTMLButtonElement>(tokenJournalBlockControls.nextSelector).disabled = true;
|
||||
const request: DemoSplTokenJournalRequest = {
|
||||
...tokenJournalBaseRequest,
|
||||
cursor,
|
||||
blockIndex,
|
||||
};
|
||||
try {
|
||||
const rows = await invoke<DemoSplTokenJournalRow[]>("demo_spl_token_journal", { request: tokenJournalRequest() });
|
||||
renderTokenJournal(rows);
|
||||
frontendDebug("kb-app-demo-desktop.frontend.demo_execution_spl", `Invoke demo_spl_token_journal block=${blockIndex}`);
|
||||
const payload = await invoke<DemoSplTokenJournalPagePayload>("demo_spl_token_journal", { request });
|
||||
applyStoreBlockMetadata(tokenJournalBlockState, payload.page, tokenJournalBlockControls);
|
||||
frontendDebug("kb-app-demo-desktop.frontend.demo_execution_spl", `Invoke demo_spl_token_journal completed block=${payload.page.blockIndex}/${payload.page.totalBlocks} rows=${payload.rows.length}`);
|
||||
renderTokenJournal(payload.rows);
|
||||
appendLog({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: "info",
|
||||
stage: "spl_token_journal",
|
||||
message: `${rows.length} événement(s) matérialisé(s) chargé(s).`,
|
||||
message: `${payload.rows.length} événement(s) matérialisé(s) chargé(s) dans le bloc ${payload.page.blockIndex}/${payload.page.totalBlocks}.`,
|
||||
});
|
||||
} catch (caughtError) {
|
||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||
@@ -851,12 +934,18 @@ async function loadTokenJournal(): Promise<void> {
|
||||
message,
|
||||
error: message,
|
||||
}, null, 2);
|
||||
renderStoreBlockControls(tokenJournalBlockState, tokenJournalBlockControls);
|
||||
frontendError("kb-app-demo-desktop.frontend.demo_execution_spl", `Token journal loading failed: ${message}`);
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function markTokenJournalFiltersDirty(): void {
|
||||
tokenJournalBaseRequest = null;
|
||||
markStoreBlockFiltersDirty(tokenJournalBlockState, tokenJournalBlockControls);
|
||||
}
|
||||
|
||||
async function generateRecipient(): Promise<void> {
|
||||
try {
|
||||
const payload = await invoke<DemoExecutionSolanaCoreGeneratedRecipientPayload>("demo_execution_solana_core_generate_recipient");
|
||||
@@ -898,7 +987,9 @@ async function prepareSelectedProfile(): Promise<void> {
|
||||
}
|
||||
appendLog({ timestamp: new Date().toISOString(), level: "info", stage: "store_prepare", message: `Vérification du store du profil ${profile.name}.` });
|
||||
try {
|
||||
frontendDebug("kb-app-demo-desktop.frontend.demo_execution_spl", `Invoke demo_execution_devnet_prepare_profile profile=${profile.name}`);
|
||||
const readiness = await invoke<DemoExecutionDevnetStoreReadinessPayload>("demo_execution_devnet_prepare_profile", { profileName: profile.name });
|
||||
frontendDebug("kb-app-demo-desktop.frontend.demo_execution_spl", `Invoke demo_execution_devnet_prepare_profile completed profile=${profile.name} resources=${readiness.availableResourcesAfter}/${readiness.expectedResources}`);
|
||||
profileStoreReady = true;
|
||||
appendLog({ timestamp: new Date().toISOString(), level: "info", stage: "store_ready", message: `Store ${readiness.backend} prêt: ${readiness.availableResourcesAfter}/${readiness.expectedResources} ressources, créées=${readiness.createdResources}, autoInit=${readiness.autoInitializeSchema}.` });
|
||||
} catch (caughtError) {
|
||||
@@ -948,6 +1039,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(item => new bootstrap.Tooltip(item));
|
||||
element<HTMLSelectElement>("#executionProfileSelect").addEventListener("change", () => {
|
||||
updateProfileHelp();
|
||||
markTokenJournalFiltersDirty();
|
||||
markAtaJournalFiltersDirty();
|
||||
void prepareSelectedProfile();
|
||||
});
|
||||
element<HTMLButtonElement>("#generateRecipientButton").addEventListener("click", () => {
|
||||
@@ -982,8 +1075,27 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
void executeToken2022(true);
|
||||
});
|
||||
element<HTMLButtonElement>("#refreshTokenJournalButton").addEventListener("click", () => {
|
||||
void loadTokenJournal();
|
||||
frontendDebug("kb-app-demo-desktop.frontend.demo_execution_spl", "Token journal load button clicked");
|
||||
void startTokenJournalLoad();
|
||||
});
|
||||
element<HTMLButtonElement>("#tokenJournalFirstBlockButton").addEventListener("click", () => {
|
||||
void loadTokenJournalBlock(1);
|
||||
});
|
||||
element<HTMLButtonElement>("#tokenJournalPreviousBlockButton").addEventListener("click", () => {
|
||||
void loadTokenJournalBlock(tokenJournalBlockState.currentBlock - 1);
|
||||
});
|
||||
element<HTMLButtonElement>("#tokenJournalNextBlockButton").addEventListener("click", () => {
|
||||
void loadTokenJournalBlock(tokenJournalBlockState.currentBlock + 1);
|
||||
});
|
||||
for (const selector of [
|
||||
"#tokenJournalSignatureInput",
|
||||
"#tokenJournalMintInput",
|
||||
"#tokenJournalAccountInput",
|
||||
"#tokenJournalOperationInput",
|
||||
]) {
|
||||
element<HTMLInputElement>(selector).addEventListener("input", markTokenJournalFiltersDirty);
|
||||
}
|
||||
element<HTMLSelectElement>("#tokenJournalFamilySelect").addEventListener("change", markTokenJournalFiltersDirty);
|
||||
element<HTMLButtonElement>("#deriveAtaButton").addEventListener("click", () => {
|
||||
void deriveAta();
|
||||
});
|
||||
@@ -994,8 +1106,26 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
void executeAta(true);
|
||||
});
|
||||
element<HTMLButtonElement>("#refreshAtaJournalButton").addEventListener("click", () => {
|
||||
void loadAtaJournal();
|
||||
frontendDebug("kb-app-demo-desktop.frontend.demo_execution_spl", "ATA journal load button clicked");
|
||||
void startAtaJournalLoad();
|
||||
});
|
||||
element<HTMLButtonElement>("#ataJournalFirstBlockButton").addEventListener("click", () => {
|
||||
void loadAtaJournalBlock(1);
|
||||
});
|
||||
element<HTMLButtonElement>("#ataJournalPreviousBlockButton").addEventListener("click", () => {
|
||||
void loadAtaJournalBlock(ataJournalBlockState.currentBlock - 1);
|
||||
});
|
||||
element<HTMLButtonElement>("#ataJournalNextBlockButton").addEventListener("click", () => {
|
||||
void loadAtaJournalBlock(ataJournalBlockState.currentBlock + 1);
|
||||
});
|
||||
for (const selector of [
|
||||
"#ataJournalSignatureInput",
|
||||
"#ataJournalMintInput",
|
||||
"#ataJournalAddressInput",
|
||||
"#ataJournalOperationInput",
|
||||
]) {
|
||||
element<HTMLInputElement>(selector).addEventListener("input", markAtaJournalFiltersDirty);
|
||||
}
|
||||
element<HTMLButtonElement>("#cancelExecutionButton").addEventListener("click", () => {
|
||||
void cancelExecution();
|
||||
});
|
||||
|
||||
114
kb-app-demo-desktop/frontend/ts/demo_store_block_pagination.ts
Normal file
114
kb-app-demo-desktop/frontend/ts/demo_store_block_pagination.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
// file: kb-app-demo-desktop/frontend/ts/demo_store_block_pagination.ts
|
||||
// version: 1
|
||||
|
||||
export type StoreBlockMetadata = {
|
||||
blockSize: number;
|
||||
blockIndex: number;
|
||||
totalRows: string;
|
||||
totalBlocks: number;
|
||||
nextCursor: string | null;
|
||||
};
|
||||
|
||||
export type StoreBlockState = {
|
||||
blockSize: number;
|
||||
currentBlock: number;
|
||||
totalBlocks: number;
|
||||
totalRows: string;
|
||||
nextCursor: string | null;
|
||||
cursors: Map<number, string | null>;
|
||||
};
|
||||
|
||||
export type StoreBlockControls = {
|
||||
firstSelector: string;
|
||||
previousSelector: string;
|
||||
nextSelector: string;
|
||||
summarySelector: string;
|
||||
};
|
||||
|
||||
export function createStoreBlockState(): StoreBlockState {
|
||||
return {
|
||||
blockSize: 500,
|
||||
currentBlock: 0,
|
||||
totalBlocks: 0,
|
||||
totalRows: "0",
|
||||
nextCursor: null,
|
||||
cursors: new Map<number, string | null>([[1, null]]),
|
||||
};
|
||||
}
|
||||
|
||||
export function resetStoreBlockState(state: StoreBlockState): void {
|
||||
state.blockSize = 500;
|
||||
state.currentBlock = 0;
|
||||
state.totalBlocks = 0;
|
||||
state.totalRows = "0";
|
||||
state.nextCursor = null;
|
||||
state.cursors = new Map<number, string | null>([[1, null]]);
|
||||
}
|
||||
|
||||
export function applyStoreBlockMetadata(
|
||||
state: StoreBlockState,
|
||||
metadata: StoreBlockMetadata,
|
||||
controls: StoreBlockControls,
|
||||
): void {
|
||||
state.blockSize = metadata.blockSize;
|
||||
state.currentBlock = metadata.blockIndex;
|
||||
state.totalBlocks = metadata.totalBlocks;
|
||||
state.totalRows = metadata.totalRows;
|
||||
state.nextCursor = metadata.nextCursor;
|
||||
if (metadata.blockIndex > 0 && metadata.nextCursor !== null) {
|
||||
state.cursors.set(metadata.blockIndex + 1, metadata.nextCursor);
|
||||
}
|
||||
renderStoreBlockControls(state, controls);
|
||||
}
|
||||
|
||||
export function renderStoreBlockControls(
|
||||
state: StoreBlockState,
|
||||
controls: StoreBlockControls,
|
||||
): void {
|
||||
const firstButton = document.querySelector<HTMLButtonElement>(controls.firstSelector);
|
||||
const previousButton = document.querySelector<HTMLButtonElement>(controls.previousSelector);
|
||||
const nextButton = document.querySelector<HTMLButtonElement>(controls.nextSelector);
|
||||
const summary = document.querySelector<HTMLElement>(controls.summarySelector);
|
||||
const hasRows = state.totalBlocks > 0;
|
||||
if (firstButton !== null) {
|
||||
firstButton.disabled = !hasRows || state.currentBlock <= 1;
|
||||
}
|
||||
if (previousButton !== null) {
|
||||
previousButton.disabled = !hasRows || state.currentBlock <= 1;
|
||||
}
|
||||
if (nextButton !== null) {
|
||||
nextButton.disabled = state.nextCursor === null;
|
||||
}
|
||||
if (summary !== null) {
|
||||
summary.textContent = hasRows
|
||||
? `Bloc ${state.currentBlock} / ${state.totalBlocks} · ${state.totalRows} résultat(s) · ${state.blockSize}/bloc`
|
||||
: `Bloc 0 / 0 · 0 résultat · ${state.blockSize}/bloc`;
|
||||
}
|
||||
}
|
||||
|
||||
export function markStoreBlockFiltersDirty(
|
||||
state: StoreBlockState,
|
||||
controls: StoreBlockControls,
|
||||
): void {
|
||||
resetStoreBlockState(state);
|
||||
const firstButton = document.querySelector<HTMLButtonElement>(controls.firstSelector);
|
||||
const previousButton = document.querySelector<HTMLButtonElement>(controls.previousSelector);
|
||||
const nextButton = document.querySelector<HTMLButtonElement>(controls.nextSelector);
|
||||
const summary = document.querySelector<HTMLElement>(controls.summarySelector);
|
||||
if (firstButton !== null) {
|
||||
firstButton.disabled = true;
|
||||
}
|
||||
if (previousButton !== null) {
|
||||
previousButton.disabled = true;
|
||||
}
|
||||
if (nextButton !== null) {
|
||||
nextButton.disabled = true;
|
||||
}
|
||||
if (summary !== null) {
|
||||
summary.textContent = "Filtres modifiés — rechargez le premier bloc.";
|
||||
}
|
||||
}
|
||||
|
||||
export function cursorForBlock(state: StoreBlockState, blockIndex: number): string | null | undefined {
|
||||
return state.cursors.get(blockIndex);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-app-demo-desktop/frontend/ts/demo_store_core.ts
|
||||
// version: 6
|
||||
// version: 7
|
||||
|
||||
import * as bootstrap from "bootstrap";
|
||||
import "simplebar";
|
||||
@@ -35,7 +35,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
frontendDebug("kb_app_demo_desktop.frontend.demo_store_core", "Core store 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", () => {
|
||||
document.querySelector<HTMLButtonElement>("#refreshStoreCoreButton")?.addEventListener("click", () => {
|
||||
void refreshCore();
|
||||
});
|
||||
void refreshCore();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-app-demo-desktop/frontend/ts/demo_store_diag.ts
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
import * as bootstrap from "bootstrap";
|
||||
import "simplebar";
|
||||
@@ -50,10 +50,10 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
frontendDebug("kb_app_demo_desktop.frontend.demo_store_diag", "Store 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", () => {
|
||||
document.querySelector<HTMLButtonElement>("#refreshStoreDiagButton")?.addEventListener("click", () => {
|
||||
void refreshDiagnostics();
|
||||
});
|
||||
document.querySelector<HTMLButtonElement>("#copySqlDiagButton")?.addEventListener("click", () => {
|
||||
document.querySelector<HTMLButtonElement>("#copyStoreDiagButton")?.addEventListener("click", () => {
|
||||
void copyDiagnostics();
|
||||
});
|
||||
void refreshDiagnostics();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-app-demo-desktop/frontend/ts/demo_store_raw.ts
|
||||
// version: 6
|
||||
// version: 7
|
||||
|
||||
import * as bootstrap from "bootstrap";
|
||||
import "simplebar";
|
||||
@@ -35,7 +35,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
frontendDebug("kb_app_demo_desktop.frontend.demo_store_raw", "Raw store 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", () => {
|
||||
document.querySelector<HTMLButtonElement>("#refreshStoreRawButton")?.addEventListener("click", () => {
|
||||
void refreshRaw();
|
||||
});
|
||||
void refreshRaw();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-app-demo-desktop/frontend/ts/demo_store_replay_candidates.ts
|
||||
// version: 12
|
||||
// version: 14
|
||||
|
||||
import * as bootstrap from "bootstrap";
|
||||
import "simplebar";
|
||||
@@ -10,12 +10,25 @@ 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 {
|
||||
applyStoreBlockMetadata,
|
||||
createStoreBlockState,
|
||||
cursorForBlock,
|
||||
markStoreBlockFiltersDirty,
|
||||
resetStoreBlockState,
|
||||
renderStoreBlockControls,
|
||||
type StoreBlockControls,
|
||||
type StoreBlockState,
|
||||
} from "./demo_store_block_pagination";
|
||||
import type { DemoStoreReplayEntityPagePayload } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayEntityPagePayload";
|
||||
import type { DemoStoreReplayEntityRequest } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayEntityRequest";
|
||||
import type { DemoStoreReplayEntityRow } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayEntityRow";
|
||||
import type { DemoStoreReplayKnownProgramOption } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayKnownProgramOption";
|
||||
import type { DemoStoreReplayOptionsPayload } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayOptionsPayload";
|
||||
import type { DemoStoreReplayProgramPagePayload } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayProgramPagePayload";
|
||||
import type { DemoStoreReplayProgramRequest } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayProgramRequest";
|
||||
import type { DemoStoreReplayProgramRow } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayProgramRow";
|
||||
import type { DemoStoreReplayTransactionPagePayload } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayTransactionPagePayload";
|
||||
import type { DemoStoreReplayTransactionRequest } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayTransactionRequest";
|
||||
import type { DemoStoreReplayTransactionRow } from "./bindings/kb_app_demo_desktop/demo_store_replay_candidates/DemoStoreReplayTransactionRow";
|
||||
|
||||
@@ -30,7 +43,7 @@ type EntityViewConfig = {
|
||||
pluralLabel: string;
|
||||
tableSelector: string;
|
||||
containsSelector: string;
|
||||
limitSelector: string;
|
||||
blockControls: StoreBlockControls;
|
||||
summarySelector: string;
|
||||
loadButtonSelector: string;
|
||||
resetButtonSelector: string;
|
||||
@@ -48,7 +61,12 @@ const entityViewConfigs: EntityViewConfig[] = [
|
||||
pluralLabel: "mints",
|
||||
tableSelector: "#mintCandidateTable",
|
||||
containsSelector: "#mintContainsInput",
|
||||
limitSelector: "#mintLimitInput",
|
||||
blockControls: {
|
||||
firstSelector: "#mintFirstBlockButton",
|
||||
previousSelector: "#mintPreviousBlockButton",
|
||||
nextSelector: "#mintNextBlockButton",
|
||||
summarySelector: "#mintBlockSummary",
|
||||
},
|
||||
summarySelector: "#mintResultSummary",
|
||||
loadButtonSelector: "#loadMintsButton",
|
||||
resetButtonSelector: "#resetMintsButton",
|
||||
@@ -63,7 +81,12 @@ const entityViewConfigs: EntityViewConfig[] = [
|
||||
pluralLabel: "owners",
|
||||
tableSelector: "#ownerCandidateTable",
|
||||
containsSelector: "#ownerContainsInput",
|
||||
limitSelector: "#ownerLimitInput",
|
||||
blockControls: {
|
||||
firstSelector: "#ownerFirstBlockButton",
|
||||
previousSelector: "#ownerPreviousBlockButton",
|
||||
nextSelector: "#ownerNextBlockButton",
|
||||
summarySelector: "#ownerBlockSummary",
|
||||
},
|
||||
summarySelector: "#ownerResultSummary",
|
||||
loadButtonSelector: "#loadOwnersButton",
|
||||
resetButtonSelector: "#resetOwnersButton",
|
||||
@@ -78,7 +101,12 @@ const entityViewConfigs: EntityViewConfig[] = [
|
||||
pluralLabel: "comptes",
|
||||
tableSelector: "#accountCandidateTable",
|
||||
containsSelector: "#accountContainsInput",
|
||||
limitSelector: "#accountLimitInput",
|
||||
blockControls: {
|
||||
firstSelector: "#accountFirstBlockButton",
|
||||
previousSelector: "#accountPreviousBlockButton",
|
||||
nextSelector: "#accountNextBlockButton",
|
||||
summarySelector: "#accountBlockSummary",
|
||||
},
|
||||
summarySelector: "#accountResultSummary",
|
||||
loadButtonSelector: "#loadAccountsButton",
|
||||
resetButtonSelector: "#resetAccountsButton",
|
||||
@@ -94,7 +122,31 @@ let mintTable: Api<DemoStoreReplayEntityRow> | null = null;
|
||||
let ownerTable: Api<DemoStoreReplayEntityRow> | null = null;
|
||||
let accountTable: Api<DemoStoreReplayEntityRow> | null = null;
|
||||
let busyOperationCount = 0;
|
||||
let maximumReplayCandidateLimit = 500;
|
||||
const transactionBlockControls: StoreBlockControls = {
|
||||
firstSelector: "#transactionFirstBlockButton",
|
||||
previousSelector: "#transactionPreviousBlockButton",
|
||||
nextSelector: "#transactionNextBlockButton",
|
||||
summarySelector: "#transactionBlockSummary",
|
||||
};
|
||||
const programBlockControls: StoreBlockControls = {
|
||||
firstSelector: "#programFirstBlockButton",
|
||||
previousSelector: "#programPreviousBlockButton",
|
||||
nextSelector: "#programNextBlockButton",
|
||||
summarySelector: "#programBlockSummary",
|
||||
};
|
||||
const transactionBlockState = createStoreBlockState();
|
||||
const programBlockState = createStoreBlockState();
|
||||
const entityBlockStates = new Map<EntityKindCode, StoreBlockState>([
|
||||
["mint", createStoreBlockState()],
|
||||
["owner", createStoreBlockState()],
|
||||
["account_key", createStoreBlockState()],
|
||||
]);
|
||||
type TransactionBaseRequest = Omit<DemoStoreReplayTransactionRequest, "cursor" | "blockIndex">;
|
||||
type ProgramBaseRequest = Omit<DemoStoreReplayProgramRequest, "cursor" | "blockIndex">;
|
||||
type EntityBaseRequest = Omit<DemoStoreReplayEntityRequest, "cursor" | "blockIndex">;
|
||||
let transactionBaseRequest: TransactionBaseRequest | null = null;
|
||||
let programBaseRequest: ProgramBaseRequest | null = null;
|
||||
const entityBaseRequests = new Map<EntityKindCode, EntityBaseRequest>();
|
||||
|
||||
function element<T extends HTMLElement>(selector: string): T {
|
||||
const value = document.querySelector<T>(selector);
|
||||
@@ -117,24 +169,6 @@ function requiredInputValue(selector: string): string {
|
||||
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) {
|
||||
@@ -285,7 +319,7 @@ function createTransactionTable(): Api<DemoStoreReplayTransactionRow> {
|
||||
],
|
||||
order: [[2, "desc"]],
|
||||
pageLength: 10,
|
||||
lengthMenu: [10, 25, 50, 100, 250],
|
||||
lengthMenu: [10, 25, 50, 100, 250, 500],
|
||||
select: { style: "multi", selector: "td:first-child", headerCheckbox: "select-all" },
|
||||
language: commonLanguage(
|
||||
"Aucune transaction chargée",
|
||||
@@ -319,7 +353,7 @@ function createProgramTable(): Api<DemoStoreReplayProgramRow> {
|
||||
],
|
||||
order: [[3, "desc"]],
|
||||
pageLength: 10,
|
||||
lengthMenu: [10, 25, 50, 100, 250],
|
||||
lengthMenu: [10, 25, 50, 100, 250, 500],
|
||||
select: { style: "multi", selector: "td:first-child", headerCheckbox: "select-all" },
|
||||
language: commonLanguage(
|
||||
"Aucun programme chargé",
|
||||
@@ -352,7 +386,7 @@ function createEntityTable(config: EntityViewConfig): Api<DemoStoreReplayEntityR
|
||||
],
|
||||
order: [[2, "desc"]],
|
||||
pageLength: 10,
|
||||
lengthMenu: [10, 25, 50, 100, 250],
|
||||
lengthMenu: [10, 25, 50, 100, 250, 500],
|
||||
select: { style: "multi", selector: "td:first-child", headerCheckbox: "select-all" },
|
||||
language: commonLanguage(
|
||||
`Aucun ${config.label} chargé`,
|
||||
@@ -412,6 +446,21 @@ function selectedOrSingleFilteredRow<T>(table: Api<T>, label: string): T {
|
||||
throw new Error(`Cochez exactement un ${label}, ou réduisez le filtre local à une seule ligne.`);
|
||||
}
|
||||
|
||||
function restoreLoadedStoreBlockControls(): void {
|
||||
if (transactionBlockState.currentBlock > 0) {
|
||||
renderStoreBlockControls(transactionBlockState, transactionBlockControls);
|
||||
}
|
||||
if (programBlockState.currentBlock > 0) {
|
||||
renderStoreBlockControls(programBlockState, programBlockControls);
|
||||
}
|
||||
for (const config of entityViewConfigs) {
|
||||
const state = entityBlockState(config.kind);
|
||||
if (state.currentBlock > 0) {
|
||||
renderStoreBlockControls(state, config.blockControls);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setBusy(busy: boolean, message: string): void {
|
||||
busyOperationCount = busy ? busyOperationCount + 1 : Math.max(0, busyOperationCount - 1);
|
||||
const operationInProgress = busyOperationCount > 0;
|
||||
@@ -419,13 +468,20 @@ function setBusy(busy: boolean, message: string): void {
|
||||
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;
|
||||
if (button.id === "openCoreExtractionButton") {
|
||||
return;
|
||||
}
|
||||
if (!operationInProgress && button.dataset.storeBlockNavigation === "true") {
|
||||
return;
|
||||
}
|
||||
button.disabled = operationInProgress;
|
||||
});
|
||||
if (!operationInProgress) {
|
||||
restoreLoadedStoreBlockControls();
|
||||
}
|
||||
}
|
||||
|
||||
function buildTransactionRequest(): DemoStoreReplayTransactionRequest {
|
||||
function buildTransactionBaseRequest(): TransactionBaseRequest {
|
||||
const entityKind = optionalInputValue("#transactionEntityKindSelect");
|
||||
const entityValue = optionalInputValue("#transactionEntityValueInput");
|
||||
if ((entityKind === null) !== (entityValue === null)) {
|
||||
@@ -441,11 +497,31 @@ function buildTransactionRequest(): DemoStoreReplayTransactionRequest {
|
||||
programScope: requiredInputValue("#transactionProgramScopeSelect"),
|
||||
entityKind,
|
||||
entityValue,
|
||||
limit: boundedPositiveIntegerValue("#transactionLimitInput"),
|
||||
newestFirst: element<HTMLInputElement>("#transactionNewestFirstInput").checked,
|
||||
};
|
||||
}
|
||||
|
||||
function buildProgramBaseRequest(): ProgramBaseRequest {
|
||||
return {
|
||||
programIdContains: optionalInputValue("#programContainsInput"),
|
||||
};
|
||||
}
|
||||
|
||||
function buildEntityBaseRequest(config: EntityViewConfig): EntityBaseRequest {
|
||||
return {
|
||||
entityKind: config.kind,
|
||||
entityValueContains: optionalInputValue(config.containsSelector),
|
||||
};
|
||||
}
|
||||
|
||||
function entityBlockState(kind: EntityKindCode): StoreBlockState {
|
||||
const state = entityBlockStates.get(kind);
|
||||
if (state === undefined) {
|
||||
throw new Error(`État de pagination Store manquant pour ${kind}.`);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
async function resetTransactionFilters(): Promise<void> {
|
||||
element<HTMLInputElement>("#transactionSignatureContainsInput").value = "";
|
||||
element<HTMLInputElement>("#transactionMinSlotInput").value = "";
|
||||
@@ -456,95 +532,190 @@ async function resetTransactionFilters(): Promise<void> {
|
||||
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();
|
||||
await startTransactionLoad();
|
||||
}
|
||||
|
||||
async function resetProgramFilters(): Promise<void> {
|
||||
element<HTMLSelectElement>("#knownProgramSelect").value = "";
|
||||
element<HTMLInputElement>("#programContainsInput").value = "";
|
||||
element<HTMLInputElement>("#programLimitInput").value = "500";
|
||||
resetTableState(programTable);
|
||||
await loadPrograms();
|
||||
await startProgramLoad();
|
||||
}
|
||||
|
||||
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);
|
||||
await startEntityLoad(config);
|
||||
}
|
||||
|
||||
async function loadTransactions(): Promise<void> {
|
||||
async function startTransactionLoad(): Promise<void> {
|
||||
transactionBaseRequest = buildTransactionBaseRequest();
|
||||
resetStoreBlockState(transactionBlockState);
|
||||
await loadTransactionBlock(1, null);
|
||||
}
|
||||
|
||||
async function loadTransactionBlock(blockIndex: number, cursor: string | null): Promise<void> {
|
||||
if (transactionTable === null) {
|
||||
return;
|
||||
}
|
||||
if (transactionBaseRequest === null) {
|
||||
throw new Error("Rechargez les transactions pour initialiser les filtres Store.");
|
||||
}
|
||||
try {
|
||||
setBusy(true, "Chargement transactions");
|
||||
const request = buildTransactionRequest();
|
||||
const rows = await invoke<DemoStoreReplayTransactionRow[]>("load_demo_store_replay_transactions", { request });
|
||||
replaceRows(transactionTable, rows);
|
||||
element<HTMLElement>("#transactionResultSummary").textContent = `${rows.length} transaction(s) chargée(s) depuis le store actif.`;
|
||||
frontendDebug(tracingTarget, `loaded ${rows.length} replay transaction candidates`);
|
||||
const request: DemoStoreReplayTransactionRequest = {
|
||||
...transactionBaseRequest,
|
||||
cursor,
|
||||
blockIndex,
|
||||
};
|
||||
const payload = await invoke<DemoStoreReplayTransactionPagePayload>(
|
||||
"load_demo_store_replay_transactions",
|
||||
{ request },
|
||||
);
|
||||
replaceRows(transactionTable, payload.rows);
|
||||
applyStoreBlockMetadata(transactionBlockState, payload.page, transactionBlockControls);
|
||||
element<HTMLElement>("#transactionResultSummary").textContent = `${payload.rows.length} transaction(s) dans le bloc Store courant.`;
|
||||
frontendDebug(tracingTarget, `loaded replay transaction block ${payload.page.blockIndex}/${payload.page.totalBlocks} with ${payload.rows.length} rows`);
|
||||
} catch (caughtError) {
|
||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||
element<HTMLElement>("#transactionResultSummary").textContent = `Erreur : ${message}`;
|
||||
renderStoreBlockControls(transactionBlockState, transactionBlockControls);
|
||||
frontendError(tracingTarget, `transaction candidate loading failed: ${message}`);
|
||||
} finally {
|
||||
setBusy(false, "Prêt");
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPrograms(): Promise<void> {
|
||||
async function startProgramLoad(): Promise<void> {
|
||||
programBaseRequest = buildProgramBaseRequest();
|
||||
resetStoreBlockState(programBlockState);
|
||||
await loadProgramBlock(1, null);
|
||||
}
|
||||
|
||||
async function loadProgramBlock(blockIndex: number, cursor: string | null): Promise<void> {
|
||||
if (programTable === null) {
|
||||
return;
|
||||
}
|
||||
if (programBaseRequest === null) {
|
||||
throw new Error("Rechargez les programmes pour initialiser les filtres Store.");
|
||||
}
|
||||
try {
|
||||
setBusy(true, "Chargement programmes");
|
||||
const request: DemoStoreReplayProgramRequest = {
|
||||
programIdContains: optionalInputValue("#programContainsInput"),
|
||||
limit: boundedPositiveIntegerValue("#programLimitInput"),
|
||||
...programBaseRequest,
|
||||
cursor,
|
||||
blockIndex,
|
||||
};
|
||||
const rows = await invoke<DemoStoreReplayProgramRow[]>("load_demo_store_replay_programs", { request });
|
||||
replaceRows(programTable, rows);
|
||||
element<HTMLElement>("#programResultSummary").textContent = `${rows.length} programme(s) chargé(s), avec occurrences top-level, inner et logs liés.`;
|
||||
frontendDebug(tracingTarget, `loaded ${rows.length} replay program summaries`);
|
||||
const payload = await invoke<DemoStoreReplayProgramPagePayload>(
|
||||
"load_demo_store_replay_programs",
|
||||
{ request },
|
||||
);
|
||||
replaceRows(programTable, payload.rows);
|
||||
applyStoreBlockMetadata(programBlockState, payload.page, programBlockControls);
|
||||
element<HTMLElement>("#programResultSummary").textContent = `${payload.rows.length} programme(s) dans le bloc Store courant, avec occurrences top-level, inner et logs liés.`;
|
||||
frontendDebug(tracingTarget, `loaded replay program block ${payload.page.blockIndex}/${payload.page.totalBlocks} with ${payload.rows.length} rows`);
|
||||
} catch (caughtError) {
|
||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||
element<HTMLElement>("#programResultSummary").textContent = `Erreur : ${message}`;
|
||||
renderStoreBlockControls(programBlockState, programBlockControls);
|
||||
frontendError(tracingTarget, `program summary loading failed: ${message}`);
|
||||
} finally {
|
||||
setBusy(false, "Prêt");
|
||||
}
|
||||
}
|
||||
|
||||
async function loadEntity(config: EntityViewConfig): Promise<void> {
|
||||
async function startEntityLoad(config: EntityViewConfig): Promise<void> {
|
||||
entityBaseRequests.set(config.kind, buildEntityBaseRequest(config));
|
||||
resetStoreBlockState(entityBlockState(config.kind));
|
||||
await loadEntityBlock(config, 1, null);
|
||||
}
|
||||
|
||||
async function loadEntityBlock(config: EntityViewConfig, blockIndex: number, cursor: string | null): Promise<void> {
|
||||
const table = entityTable(config.kind);
|
||||
if (table === null) {
|
||||
return;
|
||||
}
|
||||
const baseRequest = entityBaseRequests.get(config.kind);
|
||||
if (baseRequest === undefined) {
|
||||
throw new Error(`Rechargez les ${config.pluralLabel} pour initialiser les filtres Store.`);
|
||||
}
|
||||
try {
|
||||
setBusy(true, `Chargement ${config.pluralLabel}`);
|
||||
const request: DemoStoreReplayEntityRequest = {
|
||||
entityKind: config.kind,
|
||||
entityValueContains: optionalInputValue(config.containsSelector),
|
||||
limit: boundedPositiveIntegerValue(config.limitSelector),
|
||||
...baseRequest,
|
||||
cursor,
|
||||
blockIndex,
|
||||
};
|
||||
const rows = await invoke<DemoStoreReplayEntityRow[]>("load_demo_store_replay_entities", { request });
|
||||
replaceRows(table, rows);
|
||||
element<HTMLElement>(config.summarySelector).textContent = `${rows.length} ${config.pluralLabel} chargé(s) depuis le store actif.`;
|
||||
frontendDebug(tracingTarget, `loaded ${rows.length} replay ${config.kind} summaries`);
|
||||
const payload = await invoke<DemoStoreReplayEntityPagePayload>(
|
||||
"load_demo_store_replay_entities",
|
||||
{ request },
|
||||
);
|
||||
replaceRows(table, payload.rows);
|
||||
applyStoreBlockMetadata(entityBlockState(config.kind), payload.page, config.blockControls);
|
||||
element<HTMLElement>(config.summarySelector).textContent = `${payload.rows.length} ${config.pluralLabel} dans le bloc Store courant.`;
|
||||
frontendDebug(tracingTarget, `loaded replay ${config.kind} block ${payload.page.blockIndex}/${payload.page.totalBlocks} with ${payload.rows.length} rows`);
|
||||
} catch (caughtError) {
|
||||
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
|
||||
element<HTMLElement>(config.summarySelector).textContent = `Erreur : ${message}`;
|
||||
renderStoreBlockControls(entityBlockState(config.kind), config.blockControls);
|
||||
frontendError(tracingTarget, `${config.kind} summary loading failed: ${message}`);
|
||||
} finally {
|
||||
setBusy(false, "Prêt");
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPreviousTransactionBlock(): Promise<void> {
|
||||
const targetBlock = transactionBlockState.currentBlock - 1;
|
||||
const cursor = cursorForBlock(transactionBlockState, targetBlock);
|
||||
if (targetBlock < 1 || cursor === undefined) {
|
||||
return;
|
||||
}
|
||||
await loadTransactionBlock(targetBlock, cursor);
|
||||
}
|
||||
|
||||
async function loadNextTransactionBlock(): Promise<void> {
|
||||
if (transactionBlockState.nextCursor === null) {
|
||||
return;
|
||||
}
|
||||
await loadTransactionBlock(transactionBlockState.currentBlock + 1, transactionBlockState.nextCursor);
|
||||
}
|
||||
|
||||
async function loadPreviousProgramBlock(): Promise<void> {
|
||||
const targetBlock = programBlockState.currentBlock - 1;
|
||||
const cursor = cursorForBlock(programBlockState, targetBlock);
|
||||
if (targetBlock < 1 || cursor === undefined) {
|
||||
return;
|
||||
}
|
||||
await loadProgramBlock(targetBlock, cursor);
|
||||
}
|
||||
|
||||
async function loadNextProgramBlock(): Promise<void> {
|
||||
if (programBlockState.nextCursor === null) {
|
||||
return;
|
||||
}
|
||||
await loadProgramBlock(programBlockState.currentBlock + 1, programBlockState.nextCursor);
|
||||
}
|
||||
|
||||
async function loadPreviousEntityBlock(config: EntityViewConfig): Promise<void> {
|
||||
const state = entityBlockState(config.kind);
|
||||
const targetBlock = state.currentBlock - 1;
|
||||
const cursor = cursorForBlock(state, targetBlock);
|
||||
if (targetBlock < 1 || cursor === undefined) {
|
||||
return;
|
||||
}
|
||||
await loadEntityBlock(config, targetBlock, cursor);
|
||||
}
|
||||
|
||||
async function loadNextEntityBlock(config: EntityViewConfig): Promise<void> {
|
||||
const state = entityBlockState(config.kind);
|
||||
if (state.nextCursor === null) {
|
||||
return;
|
||||
}
|
||||
await loadEntityBlock(config, state.currentBlock + 1, state.nextCursor);
|
||||
}
|
||||
|
||||
async function copyLines(lines: string[], label: string): Promise<void> {
|
||||
if (lines.length === 0) {
|
||||
throw new Error(`Aucune ${label} à copier.`);
|
||||
@@ -635,7 +806,7 @@ async function useSelectedProgram(): Promise<void> {
|
||||
element<HTMLInputElement>("#transactionProgramIdInput").value = row.programId;
|
||||
element<HTMLSelectElement>("#transactionProgramScopeSelect").value = "any";
|
||||
showTransactionsTab();
|
||||
await loadTransactions();
|
||||
await startTransactionLoad();
|
||||
}
|
||||
|
||||
async function useSelectedEntity(config: EntityViewConfig): Promise<void> {
|
||||
@@ -647,7 +818,7 @@ async function useSelectedEntity(config: EntityViewConfig): Promise<void> {
|
||||
element<HTMLSelectElement>("#transactionEntityKindSelect").value = config.kind;
|
||||
element<HTMLInputElement>("#transactionEntityValueInput").value = row.entityValue;
|
||||
showTransactionsTab();
|
||||
await loadTransactions();
|
||||
await startTransactionLoad();
|
||||
}
|
||||
|
||||
async function openCoreExtraction(): Promise<void> {
|
||||
@@ -661,16 +832,12 @@ async function openCoreExtraction(): Promise<void> {
|
||||
|
||||
async function loadOptions(): Promise<void> {
|
||||
const options = await invoke<DemoStoreReplayOptionsPayload>("demo_store_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.");
|
||||
if (!Number.isSafeInteger(options.blockSize) || options.blockSize !== 500) {
|
||||
throw new Error("La taille de bloc Store retournée par le backend est invalide.");
|
||||
}
|
||||
maximumReplayCandidateLimit = options.maximumLimit;
|
||||
element<HTMLElement>("#replayCandidateProfileBadge").textContent = `Profil ${options.activeProfileName}`;
|
||||
element<HTMLElement>("#replayCandidateDsn").textContent = options.connectionDescriptor;
|
||||
element<HTMLElement>("#replayCandidateMaximumLimit").textContent = String(options.maximumLimit);
|
||||
document.querySelectorAll<HTMLInputElement>("input[type=number][max]").forEach(input => {
|
||||
input.max = String(options.maximumLimit);
|
||||
});
|
||||
element<HTMLElement>("#replayCandidateBlockSize").textContent = `${options.blockSize} lignes`;
|
||||
populateKnownPrograms(options.knownPrograms);
|
||||
}
|
||||
|
||||
@@ -729,14 +896,20 @@ function reportUiError(action: string, caughtError: unknown): void {
|
||||
}
|
||||
|
||||
function installHandlers(): void {
|
||||
element<HTMLButtonElement>("#loadTransactionsButton").addEventListener("click", () => void loadTransactions());
|
||||
element<HTMLButtonElement>("#loadTransactionsButton").addEventListener("click", () => void startTransactionLoad());
|
||||
element<HTMLButtonElement>("#resetTransactionsButton").addEventListener("click", () => {
|
||||
void resetTransactionFilters().catch(error => reportUiError("reset transaction filters", error));
|
||||
});
|
||||
element<HTMLButtonElement>("#loadProgramsButton").addEventListener("click", () => void loadPrograms());
|
||||
element<HTMLButtonElement>(transactionBlockControls.firstSelector).addEventListener("click", () => void loadTransactionBlock(1, null));
|
||||
element<HTMLButtonElement>(transactionBlockControls.previousSelector).addEventListener("click", () => void loadPreviousTransactionBlock());
|
||||
element<HTMLButtonElement>(transactionBlockControls.nextSelector).addEventListener("click", () => void loadNextTransactionBlock());
|
||||
element<HTMLButtonElement>("#loadProgramsButton").addEventListener("click", () => void startProgramLoad());
|
||||
element<HTMLButtonElement>("#resetProgramsButton").addEventListener("click", () => {
|
||||
void resetProgramFilters().catch(error => reportUiError("reset program filters", error));
|
||||
});
|
||||
element<HTMLButtonElement>(programBlockControls.firstSelector).addEventListener("click", () => void loadProgramBlock(1, null));
|
||||
element<HTMLButtonElement>(programBlockControls.previousSelector).addEventListener("click", () => void loadPreviousProgramBlock());
|
||||
element<HTMLButtonElement>(programBlockControls.nextSelector).addEventListener("click", () => void loadNextProgramBlock());
|
||||
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));
|
||||
@@ -754,10 +927,13 @@ function installHandlers(): void {
|
||||
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.loadButtonSelector).addEventListener("click", () => void startEntityLoad(config));
|
||||
element<HTMLButtonElement>(config.resetButtonSelector).addEventListener("click", () => {
|
||||
void resetEntityFilters(config).catch(error => reportUiError(`reset ${config.pluralLabel} filters`, error));
|
||||
});
|
||||
element<HTMLButtonElement>(config.blockControls.firstSelector).addEventListener("click", () => void loadEntityBlock(config, 1, null));
|
||||
element<HTMLButtonElement>(config.blockControls.previousSelector).addEventListener("click", () => void loadPreviousEntityBlock(config));
|
||||
element<HTMLButtonElement>(config.blockControls.nextSelector).addEventListener("click", () => void loadNextEntityBlock(config));
|
||||
element<HTMLButtonElement>(config.copyButtonSelector).addEventListener("click", () => {
|
||||
void copyLines(entityRowsForExport(config.kind).map(row => row.entityValue), config.label).catch(error => reportUiError(`copy ${config.pluralLabel}`, error));
|
||||
});
|
||||
@@ -767,16 +943,49 @@ function installHandlers(): void {
|
||||
element<HTMLButtonElement>(config.useButtonSelector).addEventListener("click", () => {
|
||||
void useSelectedEntity(config).catch(error => reportUiError(`use selected ${config.label}`, error));
|
||||
});
|
||||
element<HTMLInputElement>(config.containsSelector).addEventListener("input", () => {
|
||||
entityBaseRequests.delete(config.kind);
|
||||
markStoreBlockFiltersDirty(entityBlockState(config.kind), config.blockControls);
|
||||
});
|
||||
}
|
||||
const transactionFilterSelectors = [
|
||||
"#transactionSignatureContainsInput",
|
||||
"#transactionMinSlotInput",
|
||||
"#transactionMaxSlotInput",
|
||||
"#transactionRawStateSelect",
|
||||
"#transactionLedgerStatusSelect",
|
||||
"#transactionProgramIdInput",
|
||||
"#transactionProgramScopeSelect",
|
||||
"#transactionEntityKindSelect",
|
||||
"#transactionEntityValueInput",
|
||||
"#transactionNewestFirstInput",
|
||||
];
|
||||
for (const selector of transactionFilterSelectors) {
|
||||
element<HTMLInputElement | HTMLSelectElement>(selector).addEventListener("change", () => {
|
||||
transactionBaseRequest = null;
|
||||
markStoreBlockFiltersDirty(transactionBlockState, transactionBlockControls);
|
||||
});
|
||||
element<HTMLInputElement | HTMLSelectElement>(selector).addEventListener("input", () => {
|
||||
transactionBaseRequest = null;
|
||||
markStoreBlockFiltersDirty(transactionBlockState, transactionBlockControls);
|
||||
});
|
||||
}
|
||||
element<HTMLInputElement>("#programContainsInput").addEventListener("input", () => {
|
||||
programBaseRequest = null;
|
||||
markStoreBlockFiltersDirty(programBlockState, programBlockControls);
|
||||
});
|
||||
element<HTMLSelectElement>("#knownProgramSelect").addEventListener("change", event => {
|
||||
const target = event.currentTarget as HTMLSelectElement;
|
||||
element<HTMLInputElement>("#programContainsInput").value = target.value;
|
||||
programBaseRequest = null;
|
||||
markStoreBlockFiltersDirty(programBlockState, programBlockControls);
|
||||
});
|
||||
document.querySelectorAll<HTMLButtonElement>('[data-bs-toggle="tab"]').forEach(button => {
|
||||
button.addEventListener("shown.bs.tab", adjustVisibleTables);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
installFrontendConsoleBridge(tracingTarget);
|
||||
frontendDebug(tracingTarget, "Store replay candidate browser loaded");
|
||||
@@ -790,11 +999,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
installInlineCopyHandler();
|
||||
void loadOptions()
|
||||
.then(() => Promise.all([
|
||||
loadTransactions(),
|
||||
loadPrograms(),
|
||||
loadEntity(entityViewConfigs[0]),
|
||||
loadEntity(entityViewConfigs[1]),
|
||||
loadEntity(entityViewConfigs[2]),
|
||||
startTransactionLoad(),
|
||||
startProgramLoad(),
|
||||
startEntityLoad(entityViewConfigs[0]),
|
||||
startEntityLoad(entityViewConfigs[1]),
|
||||
startEntityLoad(entityViewConfigs[2]),
|
||||
]))
|
||||
.catch(caughtError => reportUiError("initial loading", caughtError));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user