v0.1.4-pre.010
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-app-config-desk/frontend/main.html -->
|
||||
<!-- version: 4 -->
|
||||
<!-- version: 5 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
@@ -116,6 +116,94 @@
|
||||
<div id="documentsStatus" class="alert alert-primary mb-0" role="status" aria-live="polite">Chargement de l'inventaire Config...</div>
|
||||
</section>
|
||||
|
||||
|
||||
<section id="profilesPanel" hidden aria-label="Profils Config">
|
||||
<div class="d-flex flex-wrap gap-3 align-items-end justify-content-between mb-3">
|
||||
<div class="flex-grow-1">
|
||||
<h2 class="h5 mb-2">Inspection des profils</h2>
|
||||
<p class="text-body-secondary small mb-3">La sélection, la fusion global/profil et la résolution environnementale restent sous contrôle de ksp-config-lib.</p>
|
||||
<div class="row g-2">
|
||||
<div class="col-xl-6">
|
||||
<label class="form-label" for="profileDocumentSelect">Document Config</label>
|
||||
<select id="profileDocumentSelect" class="form-select" disabled></select>
|
||||
</div>
|
||||
<div class="col-xl-6">
|
||||
<label class="form-label" for="profileIdSelect">Profil inspecté</label>
|
||||
<select id="profileIdSelect" class="form-select" disabled></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<button id="inspectDefaultProfile" class="btn btn-outline-secondary btn-sm" type="button">Profil par défaut</button>
|
||||
<button id="reloadProfileDetail" class="btn btn-outline-secondary btn-sm" type="button">Recharger</button>
|
||||
<button id="refreshProfiles" class="btn btn-outline-primary btn-sm" type="button">
|
||||
<i class="fa-solid fa-rotate me-1" aria-hidden="true"></i>
|
||||
Rafraîchir
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section id="profileDetailPanel" hidden class="border rounded p-3 mb-4 bg-body-tertiary" aria-label="Détail du profil Config">
|
||||
<dl class="row small mb-4 app-profile-metadata">
|
||||
<dt class="col-lg-3">file_id</dt>
|
||||
<dd id="profileDetailFileId" class="col-lg-9">—</dd>
|
||||
<dt class="col-lg-3">Path résolu</dt>
|
||||
<dd id="profileDetailPath" class="col-lg-9">—</dd>
|
||||
<dt class="col-lg-3">default_profile</dt>
|
||||
<dd id="profileDetailDefault" class="col-lg-9">—</dd>
|
||||
<dt class="col-lg-3">Profil sélectionné</dt>
|
||||
<dd id="profileDetailSelected" class="col-lg-9">—</dd>
|
||||
<dt class="col-lg-3">Source de sélection</dt>
|
||||
<dd id="profileDetailSelectionSource" class="col-lg-9">—</dd>
|
||||
<dt class="col-lg-3 mb-0">Sensibilité effective</dt>
|
||||
<dd id="profileDetailSensitivity" class="col-lg-9 mb-0">—</dd>
|
||||
</dl>
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-xxl-4">
|
||||
<h3 class="h6">Globals source</h3>
|
||||
<pre id="profileGlobalsJson" class="app-profile-json mb-0"></pre>
|
||||
</div>
|
||||
<div class="col-xxl-4">
|
||||
<h3 class="h6">Profil source</h3>
|
||||
<pre id="profileSourceJson" class="app-profile-json mb-0"></pre>
|
||||
</div>
|
||||
<div class="col-xxl-4">
|
||||
<h3 class="h6">Effective sûre</h3>
|
||||
<pre id="profileEffectiveJson" class="app-profile-json mb-0"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-xl-5">
|
||||
<h3 class="h6">Provenance top-level</h3>
|
||||
<div class="table-responsive">
|
||||
<table id="profileOriginsTable" class="table table-sm table-striped align-middle mb-0">
|
||||
<thead>
|
||||
<tr><th scope="col">Clé</th><th scope="col">Origine</th></tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-7">
|
||||
<h3 class="h6">Provenance environnement</h3>
|
||||
<div class="table-responsive">
|
||||
<table id="profileEnvironmentProvenanceTable" class="table table-sm table-striped align-middle mb-0">
|
||||
<thead>
|
||||
<tr><th scope="col">JSON Pointer</th><th scope="col">Variable</th><th scope="col">Source</th><th scope="col">Sensibilité</th></tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-text mt-3">L'effective affichée utilise exclusivement la représentation sûre/redacted de Config ; aucune valeur réelle Secret n'est projetée.</div>
|
||||
</section>
|
||||
|
||||
<div id="profilesStatus" class="alert alert-primary mb-0" role="status" aria-live="polite">Chargement des profils Config...</div>
|
||||
</section>
|
||||
|
||||
<section id="placeholderPanel" hidden aria-live="polite">
|
||||
<div class="alert alert-secondary mb-4" role="status">
|
||||
Cette route du shell est prête ; son contenu fonctionnel sera ajouté dans la prerelease prévue par le plan 0.1.4.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/frontend/sass/_app.scss
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
$app-header-height: 72px;
|
||||
$app-footer-height: 42px;
|
||||
@@ -91,3 +91,23 @@ body {
|
||||
#configDocumentsTable tbody tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
.app-profile-metadata dd,
|
||||
.app-profile-json,
|
||||
#profileOriginsTable td:first-child,
|
||||
#profileEnvironmentProvenanceTable td:first-child,
|
||||
#profileEnvironmentProvenanceTable td:nth-child(2) {
|
||||
font-family: var(--bs-font-monospace);
|
||||
}
|
||||
|
||||
.app-profile-json {
|
||||
min-height: 280px;
|
||||
max-height: 460px;
|
||||
overflow: auto;
|
||||
padding: 0.875rem;
|
||||
border: 1px solid var(--bs-border-color);
|
||||
border-radius: var(--bs-border-radius);
|
||||
background: var(--bs-body-bg);
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/frontend/ts/main.ts
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
import "bootstrap";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
@@ -8,6 +8,7 @@ import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import type { AppSnapshotDto } from "./bindings/ksp_app_config_desk/dto_common/AppSnapshotDto.ts";
|
||||
import { frontendDebug, frontendInfo, frontendTrace, installFrontendConsoleBridge } from "./frontend_log";
|
||||
import { initializeDocumentsPanel } from "./documents";
|
||||
import { initializeProfilesPanel } from "./profiles";
|
||||
import { invokeKsp } from "./invoke";
|
||||
|
||||
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
|
||||
@@ -26,7 +27,7 @@ const viewCopy: Record<ViewId, { title: string; description: string }> = {
|
||||
},
|
||||
profiles: {
|
||||
title: "Profils",
|
||||
description: "Sélection et provenance des profils Config — surface fonctionnelle prévue en pre.010.",
|
||||
description: "Sélection default/explicite, vues global/profil/effective sûre et provenance Config.",
|
||||
},
|
||||
environment: {
|
||||
title: "Environnement / .env",
|
||||
@@ -74,9 +75,13 @@ function activateView(viewId: ViewId, source: "startup" | "user"): void {
|
||||
if (documents) {
|
||||
documents.hidden = viewId !== "documents";
|
||||
}
|
||||
const profiles = document.querySelector<HTMLElement>("#profilesPanel");
|
||||
if (profiles) {
|
||||
profiles.hidden = viewId !== "profiles";
|
||||
}
|
||||
const placeholder = document.querySelector<HTMLElement>("#placeholderPanel");
|
||||
if (placeholder) {
|
||||
placeholder.hidden = viewId === "overview" || viewId === "documents";
|
||||
placeholder.hidden = viewId === "overview" || viewId === "documents" || viewId === "profiles";
|
||||
}
|
||||
frontendTrace("main", "Main view DOM updated", { viewId, source });
|
||||
}
|
||||
@@ -136,6 +141,7 @@ async function initializeMain(): Promise<void> {
|
||||
frontendInfo("main", "Config Desk main frontend loaded", { windowLabel });
|
||||
bindNavigation();
|
||||
initializeDocumentsPanel();
|
||||
initializeProfilesPanel();
|
||||
activateView("overview", "startup");
|
||||
try {
|
||||
await loadSnapshot();
|
||||
|
||||
261
crates/ksp-app-config-desk/frontend/ts/profiles.ts
Normal file
261
crates/ksp-app-config-desk/frontend/ts/profiles.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
// 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();
|
||||
}
|
||||
Reference in New Issue
Block a user