Files
khadhroony-solana-project/crates/ksp-app-config-desk/frontend/ts/profiles.ts
2026-08-16 15:04:58 +02:00

262 lines
10 KiB
TypeScript

// file: crates/ksp-app-config-desk/frontend/ts/profiles.ts
// version: 1
import type { ConfigProfileDetailDto } from "./bindings/ksp_app_config_desk/profiles/ConfigProfileDetailDto.ts";
import type { ConfigProfileDocumentDto } from "./bindings/ksp_app_config_desk/profiles/ConfigProfileDocumentDto.ts";
import { frontendDebug, frontendTrace } from "./frontend_log";
import { invokeKsp } from "./invoke";
let profileDocuments: ConfigProfileDocumentDto[] = [];
let activeDetail: ConfigProfileDetailDto | null = null;
function setProfilesStatus(message: string, tone: "primary" | "success" | "warning" | "danger" = "primary"): void {
const status = document.querySelector<HTMLElement>("#profilesStatus");
if (!status) {
return;
}
status.className = `alert alert-${tone} mb-0`;
status.textContent = message;
frontendTrace("main", "Profiles panel status replaced", { tone });
}
function setText(selector: string, value: string): void {
const element = document.querySelector<HTMLElement>(selector);
if (element) {
element.textContent = value;
}
}
function setCode(selector: string, value: string): void {
const element = document.querySelector<HTMLElement>(selector);
if (element) {
element.textContent = value;
}
}
function sourceLabel(source: string): string {
if (source === "default_profile") {
return "default_profile";
}
if (source === "explicit") {
return "sélection explicite";
}
if (source === "composite") {
return "composite";
}
return source;
}
function sensitivityLabel(value: string): string {
if (value === "public") {
return "Public";
}
if (value === "internal") {
return "Internal";
}
if (value === "secret") {
return "Secret";
}
return value;
}
function populateDocumentSelect(documents: ConfigProfileDocumentDto[]): void {
const select = document.querySelector<HTMLSelectElement>("#profileDocumentSelect");
if (!select) {
return;
}
select.replaceChildren();
for (const item of documents) {
const option = document.createElement("option");
option.value = item.fileId;
option.textContent = `${item.fileId}${item.defaultProfile}`;
select.append(option);
}
select.disabled = documents.length === 0;
frontendTrace("main", "Profile document select DOM replaced", { documentCount: documents.length });
}
function populateProfileSelect(detail: ConfigProfileDetailDto): void {
const select = document.querySelector<HTMLSelectElement>("#profileIdSelect");
if (!select) {
return;
}
select.replaceChildren();
for (const profileId of detail.profileIds) {
const option = document.createElement("option");
option.value = profileId;
option.textContent = profileId === detail.defaultProfile ? `${profileId} (défaut)` : profileId;
option.selected = profileId === detail.selectedProfile;
select.append(option);
}
select.disabled = detail.profileIds.length === 0;
frontendTrace("main", "Profile id select DOM replaced", { profileCount: detail.profileIds.length });
}
function renderValueOrigins(detail: ConfigProfileDetailDto): void {
const body = document.querySelector<HTMLTableSectionElement>("#profileOriginsTable tbody");
if (!body) {
return;
}
body.replaceChildren();
for (const entry of detail.valueOrigins) {
const row = document.createElement("tr");
const key = document.createElement("td");
const origin = document.createElement("td");
key.textContent = entry.key;
origin.textContent = entry.origin === "global" ? "global" : entry.origin === "profile" ? "profil" : entry.origin;
row.append(key, origin);
body.append(row);
}
frontendTrace("main", "Profile top-level provenance table DOM replaced", { entryCount: detail.valueOrigins.length });
}
function renderEnvironmentProvenance(detail: ConfigProfileDetailDto): void {
const body = document.querySelector<HTMLTableSectionElement>("#profileEnvironmentProvenanceTable tbody");
if (!body) {
return;
}
body.replaceChildren();
for (const entry of detail.environmentProvenance) {
const row = document.createElement("tr");
for (const value of [entry.jsonPointer, entry.variableName, entry.source, sensitivityLabel(entry.sensitivity)]) {
const cell = document.createElement("td");
cell.textContent = value;
row.append(cell);
}
body.append(row);
}
if (detail.environmentProvenance.length === 0) {
const row = document.createElement("tr");
const cell = document.createElement("td");
cell.colSpan = 4;
cell.className = "text-body-secondary text-center";
cell.textContent = "Aucune substitution d'environnement dans l'effective sélectionnée.";
row.append(cell);
body.append(row);
}
frontendTrace("main", "Profile environment provenance table DOM replaced", { entryCount: detail.environmentProvenance.length });
}
function renderDetail(detail: ConfigProfileDetailDto): void {
activeDetail = detail;
populateProfileSelect(detail);
setText("#profileDetailFileId", detail.fileId);
setText("#profileDetailPath", detail.path);
setText("#profileDetailDefault", detail.defaultProfile);
setText("#profileDetailSelected", detail.selectedProfile);
setText("#profileDetailSelectionSource", sourceLabel(detail.selectionSource));
setText("#profileDetailSensitivity", sensitivityLabel(detail.effectiveSensitivity));
setCode("#profileGlobalsJson", detail.globalsJson);
setCode("#profileSourceJson", detail.profileJson);
setCode("#profileEffectiveJson", detail.effectiveSafeJson);
renderValueOrigins(detail);
renderEnvironmentProvenance(detail);
const detailPanel = document.querySelector<HTMLElement>("#profileDetailPanel");
if (detailPanel) {
detailPanel.hidden = false;
}
frontendTrace("main", "Config profile detail rendered", {
fileId: detail.fileId,
selectedProfile: detail.selectedProfile,
selectionSource: detail.selectionSource,
effectiveSensitivity: detail.effectiveSensitivity,
});
}
function activeDocumentId(): string | null {
const select = document.querySelector<HTMLSelectElement>("#profileDocumentSelect");
if (!select || select.value.length === 0) {
return null;
}
return select.value;
}
async function loadProfile(fileId: string, profileId: string | null, source: "default" | "explicit" | "reload"): Promise<void> {
frontendDebug("main", "Config profile inspection requested", { fileId, profileId, source });
setProfilesStatus(`Résolution du profil ${profileId ?? "par défaut"}...`, "primary");
try {
const detail = await invokeKsp<ConfigProfileDetailDto>("main", "get_config_profile_detail", {
fileId,
profileId,
});
renderDetail(detail);
setProfilesStatus(
`${detail.fileId} / ${detail.selectedProfile} résolu via ${sourceLabel(detail.selectionSource)}; effective sûre affichée.`,
"success",
);
frontendDebug("main", "Config profile inspection completed", {
fileId: detail.fileId,
selectedProfile: detail.selectedProfile,
selectionSource: detail.selectionSource,
});
} catch {
setProfilesStatus("Le profil Config n'a pas pu être résolu par le backend.", "danger");
}
}
async function refreshProfileDocuments(): Promise<void> {
frontendDebug("main", "Config profile document inventory refresh requested");
try {
profileDocuments = await invokeKsp<ConfigProfileDocumentDto[]>("main", "get_config_profile_documents");
populateDocumentSelect(profileDocuments);
if (profileDocuments.length === 0) {
activeDetail = null;
const detailPanel = document.querySelector<HTMLElement>("#profileDetailPanel");
if (detailPanel) {
detailPanel.hidden = true;
}
setProfilesStatus("Aucun document Config avec profils standard n'est enregistré.", "warning");
return;
}
await loadProfile(profileDocuments[0].fileId, null, "default");
} catch {
setProfilesStatus("L'inventaire des profils Config n'a pas pu être chargé.", "danger");
}
}
function bindProfileActions(): void {
document.querySelector<HTMLButtonElement>("#refreshProfiles")?.addEventListener("click", () => {
frontendTrace("main", "Profiles refresh button clicked");
void refreshProfileDocuments();
});
document.querySelector<HTMLSelectElement>("#profileDocumentSelect")?.addEventListener("change", event => {
const select = event.currentTarget;
if (!(select instanceof HTMLSelectElement) || select.value.length === 0) {
return;
}
frontendTrace("main", "Profile document selection changed", { fileId: select.value });
void loadProfile(select.value, null, "default");
});
document.querySelector<HTMLSelectElement>("#profileIdSelect")?.addEventListener("change", event => {
const select = event.currentTarget;
const fileId = activeDocumentId();
if (!(select instanceof HTMLSelectElement) || !fileId || select.value.length === 0) {
return;
}
frontendTrace("main", "Explicit profile selection changed", { fileId, profileId: select.value });
void loadProfile(fileId, select.value, "explicit");
});
document.querySelector<HTMLButtonElement>("#inspectDefaultProfile")?.addEventListener("click", () => {
const fileId = activeDocumentId();
if (!fileId) {
return;
}
frontendTrace("main", "Default profile inspection button clicked", { fileId });
void loadProfile(fileId, null, "default");
});
document.querySelector<HTMLButtonElement>("#reloadProfileDetail")?.addEventListener("click", () => {
if (!activeDetail) {
return;
}
const explicit = activeDetail.selectionSource === "explicit" ? activeDetail.selectedProfile : null;
frontendTrace("main", "Profile detail reload button clicked", { fileId: activeDetail.fileId, explicitProfile: explicit });
void loadProfile(activeDetail.fileId, explicit, "reload");
});
frontendTrace("main", "Profiles panel handlers installed");
}
export function initializeProfilesPanel(): void {
bindProfileActions();
void refreshProfileDocuments();
}