diff --git a/ui/src/main.js b/ui/src/main.js index 4675a93..0b38856 100644 --- a/ui/src/main.js +++ b/ui/src/main.js @@ -22,6 +22,19 @@ return response.json(); }; +const apiPost = async (path, payload = null) => { + const response = await fetch(`/api${path}`, { + method: "POST", + headers: payload ? { "Content-Type": "application/json" } : undefined, + body: payload ? JSON.stringify(payload) : undefined + }); + if (!response.ok) { + const text = await response.text(); + throw new Error(`${path} returned ${response.status}: ${text}`); + } + return response.json(); +}; + const AppScreen = { components: { GnBadge, @@ -52,6 +65,11 @@ const inventoryRecords = ref([]); const selectedInventoryId = ref(""); const changes = ref([]); + const selectedChangeId = ref(""); + const selectedChange = ref(null); + const changeLoading = ref(false); + const changeApplying = ref(false); + const changeError = ref(""); const gitStatus = ref(null); const tabs = [ @@ -112,6 +130,23 @@ const inventoryRecordTitle = (item) => item?.name || item?.fqdn || item?.id || "unknown"; + const selectedChangeJson = computed(() => { + if (!selectedChange.value) { + return ""; + } + return JSON.stringify(selectedChange.value, null, 2); + }); + + const changeStatusVariant = (status) => { + if (status === "applied") { + return "success"; + } + if (status === "rejected") { + return "danger"; + } + return "secondary"; + }; + const loadInventoryType = async (type) => { selectedInventoryType.value = type; selectedInventoryId.value = ""; @@ -134,6 +169,45 @@ selectedInventoryId.value = item?.id || ""; }; + const loadChange = async (id) => { + selectedChangeId.value = id; + selectedChange.value = null; + changeLoading.value = true; + changeError.value = ""; + try { + selectedChange.value = await apiGet(`/changes/${encodeURIComponent(id)}`); + } catch (caught) { + changeError.value = caught instanceof Error ? caught.message : `Failed to load change ${id}`; + } finally { + changeLoading.value = false; + } + }; + + const applySelectedChange = async () => { + if (!selectedChangeId.value) { + return; + } + changeApplying.value = true; + changeError.value = ""; + try { + selectedChange.value = await apiPost(`/changes/${encodeURIComponent(selectedChangeId.value)}/apply`); + const [changesData, validationData, freshnessData, gitData] = await Promise.all([ + apiGet("/changes"), + apiGet("/validate"), + apiGet("/health/freshness"), + apiGet("/git/status") + ]); + changes.value = changesData; + validation.value = validationData; + freshness.value = freshnessData; + gitStatus.value = gitData; + } catch (caught) { + changeError.value = caught instanceof Error ? caught.message : "Failed to apply change"; + } finally { + changeApplying.value = false; + } + }; + const load = async () => { loading.value = true; error.value = ""; @@ -162,6 +236,9 @@ inventoryTypes.value = inventoryData; changes.value = changesData; gitStatus.value = gitData; + if (!selectedChangeId.value && changesData.length) { + await loadChange(changesData[0].id); + } if (!selectedDocPath.value && docsData.length) { await loadDoc(docsData[0].path); } @@ -179,6 +256,11 @@ return { activeTab, + applySelectedChange, + changeApplying, + changeError, + changeLoading, + changeStatusVariant, changes, docError, docLoading, @@ -204,7 +286,11 @@ selectedDoc, selectedDocFrontmatterJson, selectedDocPath, + selectedChange, + selectedChangeId, + selectedChangeJson, selectInventoryRecord, + loadChange, tabs, validation, validationVariant @@ -327,11 +413,45 @@ diff --git a/ui/src/styles.css b/ui/src/styles.css index 053f615..99d9e82 100644 --- a/ui/src/styles.css +++ b/ui/src/styles.css @@ -52,7 +52,8 @@ } .gnb-inventory, -.gnb-docs { +.gnb-docs, +.gnb-changes { display: grid; grid-template-columns: minmax(220px, 320px) minmax(0, 1fr); gap: 24px; @@ -125,6 +126,7 @@ .gnb-status, .gnb-row, .gnb-docs, + .gnb-changes, .gnb-inventory, .gnb-detail-grid { grid-template-columns: 1fr;