437 lines
16 KiB
TypeScript
437 lines
16 KiB
TypeScript
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 d’une transaction.";
|
||
if (method === "sendTransaction") {
|
||
return "Transaction signée encodée en base64. Le preset fourni est volontairement invalide et sert seulement à tester la gestion d’erreur.";
|
||
}
|
||
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 d’envoi. 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");
|
||
}); |