Files
khadhroony-bobobot/kb_app/frontend/ts/demo_http.ts
2026-04-22 19:04:22 +02:00

437 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as bootstrap from "bootstrap";
import "simplebar";
import ResizeObserver from "resize-observer-polyfill";
import { invoke } from "@tauri-apps/api/core";
import { debug, takeoverConsole } from "@fltsci/tauri-plugin-tracing";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
interface DemoHttpPoolClientSnapshot {
endpointName: string;
provider: string;
endpointUrl: string;
roles: string[];
status: string;
pausedRemainingMs: number | null;
availableConcurrencySlots: number;
}
interface DemoHttpRequest {
role: string;
method: string;
firstArg: string | null;
configJson: string | null;
}
interface DemoHttpExecutionPayload {
endpointName: string;
provider: string;
endpointUrl: string;
role: string;
method: string;
methodClass: string;
responseJson: string;
}
let demoHttpLastResponseRawText = "";
function appendLogLine(textarea: HTMLTextAreaElement, line: string): void {
const now = new Date();
const timestamp = now.toLocaleTimeString("fr-CH", { hour12: false });
const lines = textarea.value === "" ? [] : textarea.value.split("\n");
lines.push(`[${timestamp}] ${line}`);
const maxLines = 400;
textarea.value = lines.slice(-maxLines).join("\n");
textarea.scrollTop = textarea.scrollHeight;
}
function methodNeedsFirstArg(method: string): boolean {
return [
"getBalance",
"getAccountInfo",
"getProgramAccounts",
"getSignaturesForAddress",
"getTransaction",
"sendTransaction",
].includes(method);
}
function firstArgLabelForMethod(method: string): string {
if (method === "getBalance") return "Address";
if (method === "getAccountInfo") return "Address";
if (method === "getProgramAccounts") return "Program id";
if (method === "getSignaturesForAddress") return "Address";
if (method === "getTransaction") return "Signature";
if (method === "sendTransaction") return "Encoded transaction (base64)";
return "First arg";
}
function firstArgHelpForMethod(method: string): string {
if (method === "getBalance") return "Adresse Solana dont on veut lire le solde.";
if (method === "getAccountInfo") return "Adresse Solana du compte à lire.";
if (method === "getProgramAccounts") return "Program id dont on veut lister les comptes.";
if (method === "getSignaturesForAddress") return "Adresse Solana dont on veut lire les signatures récentes.";
if (method === "getTransaction") return "Signature exacte dune transaction.";
if (method === "sendTransaction") {
return "Transaction signée encodée en base64. Le preset fourni est volontairement invalide et sert seulement à tester la gestion derreur.";
}
return "Adresse, program id, signature ou transaction encodée selon la méthode.";
}
function configHelpForMethod(method: string): string {
if (method === "getProgramAccounts") {
return "Preset volontairement filtré pour éviter une réponse trop large.";
}
if (method === "sendTransaction") {
return "Configuration denvoi. Avec le preset de transaction invalide, la requête doit échouer côté RPC.";
}
return "Configuration JSON optionnelle.";
}
function defaultRoleForMethod(method: string): string {
if (method === "sendTransaction") return "http_transactions";
if (method === "getProgramAccounts") return "http_heavy";
return "http_queries";
}
function defaultFirstArgForMethod(method: string): string {
if (method === "getBalance") return "11111111111111111111111111111111";
if (method === "getAccountInfo") return "11111111111111111111111111111111";
if (method === "getProgramAccounts") return "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
if (method === "getSignaturesForAddress") return "11111111111111111111111111111111";
if (method === "getTransaction") {
return "5h6xBEauJ3PK6SWC7r7J2W8mE1D7aQj4J6Jg8n1SmWnVqSg9H6gq2K7xwJkL2GZ2RZ6n9wYk9cW1b2V3a4d5e6f7";
}
if (method === "sendTransaction") return "AQ==";
return "";
}
function defaultConfigForMethod(method: string): string {
if (method === "getSlot" || method === "getBlockHeight" || method === "getLatestBlockhash") {
return `{"commitment":"confirmed"}`;
}
if (method === "getBalance") {
return `{"commitment":"confirmed"}`;
}
if (method === "getAccountInfo") {
return `{"encoding":"base64","commitment":"confirmed"}`;
}
if (method === "getProgramAccounts") {
return `{
"encoding":"base64",
"commitment":"confirmed",
"dataSlice":{"offset":0,"length":0},
"filters":[
{"dataSize":165},
{"memcmp":{"offset":32,"bytes":"11111111111111111111111111111111"}}
]
}`;
}
if (method === "getSignaturesForAddress") {
return `{"limit":10,"commitment":"confirmed"}`;
}
if (method === "getTransaction") {
return `{"encoding":"json","commitment":"confirmed","maxSupportedTransactionVersion":0}`;
}
if (method === "sendTransaction") {
return `{"encoding":"base64","skipPreflight":true,"maxRetries":0}`;
}
return "";
}
function renderPoolSnapshots(
snapshots: DemoHttpPoolClientSnapshot[],
tableBody: HTMLTableSectionElement,
): void {
tableBody.innerHTML = "";
for (const snapshot of snapshots) {
const row = document.createElement("tr");
const endpointCell = document.createElement("td");
endpointCell.innerHTML = `<div class="fw-semibold">${snapshot.endpointName}</div><div class="small text-body-secondary">${snapshot.endpointUrl}</div>`;
const providerCell = document.createElement("td");
providerCell.textContent = snapshot.provider;
const statusCell = document.createElement("td");
statusCell.textContent = snapshot.status;
const pausedCell = document.createElement("td");
pausedCell.textContent = snapshot.pausedRemainingMs === null ? "-" : String(snapshot.pausedRemainingMs);
const concurrencyCell = document.createElement("td");
concurrencyCell.textContent = String(snapshot.availableConcurrencySlots);
const rolesCell = document.createElement("td");
rolesCell.textContent = snapshot.roles.join(", ");
row.appendChild(endpointCell);
row.appendChild(providerCell);
row.appendChild(statusCell);
row.appendChild(pausedCell);
row.appendChild(concurrencyCell);
row.appendChild(rolesCell);
tableBody.appendChild(row);
}
}
function renderResponse(
responseTextarea: HTMLTextAreaElement,
prettyToggle: HTMLInputElement,
): void {
if (demoHttpLastResponseRawText.trim() === "") {
responseTextarea.value = "";
return;
}
if (!prettyToggle.checked) {
responseTextarea.value = demoHttpLastResponseRawText;
return;
}
try {
const parsed = JSON.parse(demoHttpLastResponseRawText);
responseTextarea.value = JSON.stringify(parsed, null, 2);
} catch {
responseTextarea.value = demoHttpLastResponseRawText;
}
}
async function copyTextToClipboard(text: string): Promise<void> {
if (typeof navigator !== "undefined" && navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
return;
}
const tempTextarea = document.createElement("textarea");
tempTextarea.value = text;
tempTextarea.style.position = "fixed";
tempTextarea.style.left = "-9999px";
document.body.appendChild(tempTextarea);
tempTextarea.focus();
tempTextarea.select();
document.execCommand("copy");
document.body.removeChild(tempTextarea);
}
function updateMethodForm(
roleSelect: HTMLSelectElement,
methodSelect: HTMLSelectElement,
firstArgGroup: HTMLDivElement,
firstArgLabel: HTMLLabelElement,
firstArgHelp: HTMLDivElement,
firstArgInput: HTMLInputElement,
configTextarea: HTMLTextAreaElement,
configHelp: HTMLDivElement,
): void {
const method = methodSelect.value;
firstArgGroup.style.display = methodNeedsFirstArg(method) ? "" : "none";
firstArgLabel.textContent = firstArgLabelForMethod(method);
firstArgHelp.textContent = firstArgHelpForMethod(method);
firstArgInput.value = defaultFirstArgForMethod(method);
configTextarea.value = defaultConfigForMethod(method);
configHelp.textContent = configHelpForMethod(method);
roleSelect.value = defaultRoleForMethod(method);
}
async function refreshPoolSnapshot(
tableBody: HTMLTableSectionElement,
logTextarea: HTMLTextAreaElement,
shouldLog: boolean,
): Promise<void> {
try {
const snapshots = await invoke<DemoHttpPoolClientSnapshot[]>("demo_http_list_pool_clients");
renderPoolSnapshots(snapshots, tableBody);
if (shouldLog) {
appendLogLine(logTextarea, `[ui] refreshed http pool snapshot (${snapshots.length} endpoint(s))`);
}
} catch (error) {
appendLogLine(logTextarea, `[ui] refresh pool error: ${String(error)}`);
}
}
document.addEventListener("DOMContentLoaded", async () => {
void takeoverConsole();
const sidebarToggle = document.querySelector<HTMLButtonElement>('#sidebarToggle');
if (sidebarToggle) {
// restaurer létat depuis localStorage
if (localStorage.getItem('sidebar-toggle') === 'true') {
document.body.classList.add('sidenav-toggled');
}
sidebarToggle.addEventListener('click', (event) => {
event.preventDefault();
document.body.classList.toggle('sidenav-toggled');
localStorage.setItem('sidebar-toggle', document.body.classList.contains('sidenav-toggled') ? 'true' : 'false');
});
}
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
const toastElList = document.querySelectorAll('.toast');
Array.from(toastElList).map(toastEl => new bootstrap.Toast(toastEl));
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]');
Array.from(popoverTriggerList).map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl));
const gobackto = location.pathname + location.search;
document.querySelectorAll<HTMLAnchorElement>('a[data-setlang]').forEach((a) => {
const href = a.getAttribute("href");
if (!href) return; // pas de href => on ignore
const url = new URL(href, location.origin);
url.searchParams.set("gobackto", gobackto);
// conserve une URL relative (path + query)
a.setAttribute("href", url.pathname + "?" + url.searchParams.toString());
});
const roleSelect = document.querySelector<HTMLSelectElement>("#demoHttpRoleSelect");
const methodSelect = document.querySelector<HTMLSelectElement>("#demoHttpMethodSelect");
const firstArgGroup = document.querySelector<HTMLDivElement>("#demoHttpFirstArgGroup");
const firstArgLabel = document.querySelector<HTMLLabelElement>("#demoHttpFirstArgLabel");
const firstArgHelp = document.querySelector<HTMLDivElement>("#demoHttpFirstArgHelp");
const firstArgInput = document.querySelector<HTMLInputElement>("#demoHttpFirstArgInput");
const configTextarea = document.querySelector<HTMLTextAreaElement>("#demoHttpConfigTextarea");
const configHelp = document.querySelector<HTMLDivElement>("#demoHttpConfigHelp");
const executeButton = document.querySelector<HTMLButtonElement>("#demoHttpExecuteButton");
const refreshPoolButton = document.querySelector<HTMLButtonElement>("#demoHttpRefreshPoolButton");
const clearLogButton = document.querySelector<HTMLButtonElement>("#demoHttpClearLogButton");
const copyResponseButton = document.querySelector<HTMLButtonElement>("#demoHttpCopyResponseButton");
const prettyToggle = document.querySelector<HTMLInputElement>("#demoHttpPrettyToggle");
const responseTextarea = document.querySelector<HTMLTextAreaElement>("#demoHttpResponseTextarea");
const logTextarea = document.querySelector<HTMLTextAreaElement>("#demoHttpLogTextarea");
const poolTableBody = document.querySelector<HTMLTableSectionElement>("#demoHttpPoolTableBody");
const lastEndpointText = document.querySelector<HTMLSpanElement>("#demoHttpLastEndpointText");
const lastProviderText = document.querySelector<HTMLSpanElement>("#demoHttpLastProviderText");
const lastMethodClassText = document.querySelector<HTMLSpanElement>("#demoHttpLastMethodClassText");
if (
!roleSelect ||
!methodSelect ||
!firstArgGroup ||
!firstArgLabel ||
!firstArgHelp ||
!firstArgInput ||
!configTextarea ||
!configHelp ||
!executeButton ||
!refreshPoolButton ||
!clearLogButton ||
!copyResponseButton ||
!prettyToggle ||
!responseTextarea ||
!logTextarea ||
!poolTableBody ||
!lastEndpointText ||
!lastProviderText ||
!lastMethodClassText
) {
debug("demo_http UI controls not found");
return;
}
updateMethodForm(
roleSelect,
methodSelect,
firstArgGroup,
firstArgLabel,
firstArgHelp,
firstArgInput,
configTextarea,
configHelp,
);
methodSelect.addEventListener("change", () => {
updateMethodForm(
roleSelect,
methodSelect,
firstArgGroup,
firstArgLabel,
firstArgHelp,
firstArgInput,
configTextarea,
configHelp,
);
});
prettyToggle.addEventListener("change", () => {
renderResponse(responseTextarea, prettyToggle);
});
refreshPoolButton.addEventListener("click", async () => {
await refreshPoolSnapshot(poolTableBody, logTextarea, true);
});
clearLogButton.addEventListener("click", () => {
logTextarea.value = "";
});
copyResponseButton.addEventListener("click", async () => {
if (responseTextarea.value.trim() === "") {
appendLogLine(logTextarea, "[ui] copy skipped: no response available");
return;
}
try {
await copyTextToClipboard(responseTextarea.value);
appendLogLine(logTextarea, "[ui] response copied to clipboard");
} catch (error) {
appendLogLine(logTextarea, `[ui] copy response error: ${String(error)}`);
}
});
executeButton.addEventListener("click", async () => {
const request: DemoHttpRequest = {
role: roleSelect.value,
method: methodSelect.value,
firstArg: firstArgInput.value.trim() === "" ? null : firstArgInput.value.trim(),
configJson: configTextarea.value.trim() === "" ? null : configTextarea.value.trim(),
};
try {
const response = await invoke<DemoHttpExecutionPayload>("demo_http_execute_request", { request });
lastEndpointText.textContent = `${response.endpointName} (${response.endpointUrl})`;
lastProviderText.textContent = response.provider;
lastMethodClassText.textContent = response.methodClass;
demoHttpLastResponseRawText = response.responseJson;
renderResponse(responseTextarea, prettyToggle);
appendLogLine(
logTextarea,
`[ui] ${response.method} via ${response.endpointName} / role=${response.role} / class=${response.methodClass}`,
);
await refreshPoolSnapshot(poolTableBody, logTextarea, true);
} catch (error) {
demoHttpLastResponseRawText = String(error);
renderResponse(responseTextarea, prettyToggle);
appendLogLine(logTextarea, `[ui] execute error: ${String(error)}`);
await refreshPoolSnapshot(poolTableBody, logTextarea, true);
}
});
appendLogLine(logTextarea, "[ui] demo_http window loaded");
await refreshPoolSnapshot(poolTableBody, logTextarea, true);
debug("demo_http window loaded");
});