From 7ce1c8f7ecf4523db355ab9120a509752a2a2a23 Mon Sep 17 00:00:00 2001 From: Bruce Schultz Date: Wed, 18 Mar 2026 14:14:17 +0100 Subject: [PATCH 1/4] feat(analyses): add distribution status column --- app/components/analysis/AnalysesTable.vue | 101 ++++++++++++------ .../analysis/logs/AnalysisLogCardContent.vue | 9 +- 2 files changed, 75 insertions(+), 35 deletions(-) diff --git a/app/components/analysis/AnalysesTable.vue b/app/components/analysis/AnalysesTable.vue index b9dd517..2ddafcf 100644 --- a/app/components/analysis/AnalysesTable.vue +++ b/app/components/analysis/AnalysesTable.vue @@ -7,7 +7,7 @@ import { getAnalysisNodes } from "~/composables/useAPIFetch"; import { formatDataRow } from "~/utils/format-data-row"; import { showCacheWarningToast, - showConnectionErrorToast, + showConnectionErrorToast } from "~/composables/connectionErrorToast"; import { FilterMatchMode } from "@primevue/core/api"; import SearchBar from "~/components/table/SearchBar.vue"; @@ -15,7 +15,7 @@ import AnalysisControlButtons from "./AnalysisControlButtons.vue"; import { getApprovalStatusSeverity, getBuildStatusSeverity, - getExecutionStatusSeverity, + getExecutionStatusSeverity } from "~/utils/status-tag-severity"; import { type AnalysisNode, @@ -24,7 +24,7 @@ import { type PodProgressResponse, PodStatus, type Project, - type Route, + type Route } from "~/services/Api"; import { ApprovalStatus } from "~/types/node"; import ContainerCounter from "~/components/analysis/ContainerCounter.vue"; @@ -40,12 +40,12 @@ const { nodeType, requireDataStore: datastoreRequired } = useDatastoreRequirement(); const datastoreBadgeSeverity = computed(() => - datastoreRequired.value ? "danger" : "secondary", + datastoreRequired.value ? "danger" : "secondary" ); const datastoreBadgeTooltip = computed(() => datastoreRequired.value ? "Data store missing!" - : "Data store missing, but not required", + : "Data store missing, but not required" ); const analysesMap = ref>(new Map()); @@ -61,11 +61,11 @@ const filters = ref(); // Cache const analysisCache = useState( "analysisCache", - () => undefined, + () => undefined ); const projectCache = useState( "projectCache", - () => undefined, + () => undefined ); const podOrcUnreacheable = ref(false); @@ -89,8 +89,8 @@ async function getProjects() { method: "GET", query: { sort: "-updated_at", - fields: "id,name", - }, + fields: "id,name" + } }) .catch(() => undefined)) as Project[]; } @@ -98,7 +98,7 @@ async function getProjects() { async function getKongRoutes() { const kongRoutesResp = (await useNuxtApp() .$hubApi("/kong/project", { - method: "GET", + method: "GET" }) .catch(() => undefined)) as ListRoutes; if (kongRoutesResp && kongRoutesResp.data) { @@ -156,7 +156,7 @@ async function getExecutionStatusesFromPodOrc(): Promise< > { const podOrcResponse = (await useNuxtApp() .$hubApi("/po/status", { - method: "GET", + method: "GET" }) .catch(() => { showConnectionErrorToast(toast, { @@ -164,7 +164,7 @@ async function getExecutionStatusesFromPodOrc(): Promise< summary: "Missing PO Status Update", detail: "Unable to retrieve pod statuses from the PO, relying on information from the Hub", - life: 3000, + life: 3000 }); })) as PodProgressResponse; podOrcUnreacheable.value = !podOrcResponse; @@ -216,13 +216,13 @@ function determineProgressBarColor(progress: number) { } return { - "--p-progressbar-value-background": color, + "--p-progressbar-value-background": color }; } function parseAnalysis( analysisEntry: ModifiedAnalysisNode, - executionStatuses: PodProgressResponse | undefined, + executionStatuses: PodProgressResponse | undefined ): ModifiedAnalysisNode { const projId = analysisEntry.analysis?.project_id; const analysisId = analysisEntry.analysis_id; @@ -237,7 +237,7 @@ function parseAnalysis( const acceptableHubStatuses: Array = [ PodStatus.Failed, PodStatus.Executed, - PodStatus.Finished, // Deprecated but still returned by PO + PodStatus.Finished // Deprecated but still returned by PO ]; if (executionStatuses && analysisId in executionStatuses) { const podStatus = executionStatuses[analysisId]!; @@ -254,7 +254,7 @@ function parseAnalysis( async function compileAnalysisTable( respStatus: string, - respData: AnalysisNode[] | undefined, + respData: AnalysisNode[] | undefined ) { tableLoading.value = true; await parseProjects(); @@ -275,13 +275,13 @@ async function compileAnalysisTable( const formattedAnalyses = formatDataRow( analysisData, ["created_at", "updated_at"], - expandRowEntries, + expandRowEntries ) as ModifiedAnalysisNode[]; if (formattedAnalyses && projMap.size > 0) { formattedAnalyses.forEach((analysisEntry: ModifiedAnalysisNode) => { parsedAnalyses.set( analysisEntry.analysis_id, - parseAnalysis(analysisEntry, currentExecutionStatuses), + parseAnalysis(analysisEntry, currentExecutionStatuses) ); }); analysesMap.value = parsedAnalyses; @@ -328,11 +328,11 @@ async function getNextPage() { query: { page: { offset: currentOffset, - limit: queryLimit, + limit: queryLimit }, include: "analysis,node", - sort: "-updated_at", - }, + sort: "-updated_at" + } }) .catch(() => undefined)) as AnalysisNode[]; if (nextSetResults.length > 0) { @@ -353,7 +353,7 @@ const defaultFilters = { global: { value: undefined, matchMode: FilterMatchMode.CONTAINS }, approval_status: { value: undefined, matchMode: FilterMatchMode.EQUALS }, "analysis.build_status": { value: undefined, matchMode: FilterMatchMode.IN }, - execution_status: { value: undefined, matchMode: FilterMatchMode.IN }, + execution_status: { value: undefined, matchMode: FilterMatchMode.IN } }; filters.value = defaultFilters; @@ -361,7 +361,7 @@ function resetFilters() { const clearedFilters = {}; for (const filterKey in defaultFilters) { clearedFilters[filterKey] = { - ...defaultFilters[filterKey], + ...defaultFilters[filterKey] }; clearedFilters[filterKey].value = undefined; } @@ -374,7 +374,7 @@ const updateFilters = (filterText: string) => { function updateAnalysisRun( analysisId: string, - newStatusData: AnalysisStatus | undefined, + newStatusData: AnalysisStatus | undefined ) { if (analysesMap.value.has(analysisId)) { const analysisToUpdate = analysesMap.value.get(analysisId)!; // Tell typescript we are sure there is a value @@ -396,7 +396,7 @@ function updateExecutionStatusFilter(filterText: string) { if (currentExecutionStatusFilters.includes(filterText)) { // If filter already there, then remove it const filteredStatuses = currentExecutionStatusFilters.filter( - (item) => item !== filterText, + (item) => item !== filterText ); if (filteredStatuses.length == 0) { // If empty array after filtering then set to null @@ -419,7 +419,7 @@ const showDataStoreNavToast = () => { "Unable to find an associated data store, click the button below " + "to create a data store for the project of this analysis", group: "datastoreToastLink", - life: 10000, + life: 10000 }); }; @@ -631,6 +631,40 @@ const onCloseNavToast = () => { + + + + { :showFilterOperator="false" field="execution_status" filterField="execution_status" + headerStyle="text-align: center" > diff --git a/app/components/analysis/logs/AnalysisLogCardContent.vue b/app/components/analysis/logs/AnalysisLogCardContent.vue index 19cdd5a..9a46f74 100644 --- a/app/components/analysis/logs/AnalysisLogCardContent.vue +++ b/app/components/analysis/logs/AnalysisLogCardContent.vue @@ -59,7 +59,7 @@ const copyToClipboard = async (analysisLogs: boolean) => { severity: "contrast", summary: "Copied to clipboard!", life: 3000, - group: "copiedLogs", + group: "copiedLogs" }); } catch (err) { console.error("Failed to copy: ", err); @@ -158,6 +158,7 @@ const copyToClipboard = async (analysisLogs: boolean) => { border: 1px solid grey; height: 50%; background: var(--p-slate-800); + color: #f1f5f9; } .nginx-log-card { @@ -170,15 +171,18 @@ const copyToClipboard = async (analysisLogs: boolean) => { } .log-scroll-panel { - background: #000; - font-family: - Roboto Mono Regular, - monospace; + font-family: Roboto Mono Regular, + monospace; font-size: 0.8em; height: 30em; padding: 1em; white-space: pre-wrap; word-break: break-word; +} + +.flame-dark .log-scroll-panel { + background: #000; + color: #e2e8f0; border-top: white solid 1px; } @@ -186,6 +190,7 @@ const copyToClipboard = async (analysisLogs: boolean) => { display: flex; align-items: center; background: var(--p-highlight-background); + color: var(--p-highlight-color); padding: 0.5em; border-radius: 6px; } From edbcf74cae2c521edaa93e2fbcb399e1f023988c Mon Sep 17 00:00:00 2001 From: Bruce Schultz Date: Wed, 18 Mar 2026 15:17:55 +0100 Subject: [PATCH 3/4] test(analyses): update tests for new column --- .../components/analysis/AnalysisTable.spec.ts | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/test/components/analysis/AnalysisTable.spec.ts b/test/components/analysis/AnalysisTable.spec.ts index 4e5d134..2f5215c 100644 --- a/test/components/analysis/AnalysisTable.spec.ts +++ b/test/components/analysis/AnalysisTable.spec.ts @@ -91,13 +91,13 @@ describe("AnalysesTable.vue", () => { expect(headerRow.length).toBe(1); const headerCols = headerRow[0]!.findAll("th"); expect(headerCols[0]!.text()).toBe("Name"); // First col - expect(headerCols[9]!.text()).toBe("Analysis Controls"); // Last col + expect(headerCols[10]!.text()).toBe("Analysis Controls"); // Last col // Verify the second row's content const secondRowCells = rows[1]!.findAll("td"); expect(secondRowCells[0]!.text()).toBe("T006"); // Name expect(secondRowCells[1]!.text()).toBe("approved"); // Approval status - expect(secondRowCells[7]!.text()).toBe("18.03.2025"); // Last Updated + expect(secondRowCells[8]!.text()).toBe("18.03.2025"); // Last Updated }); test("Cached results used", async () => { @@ -150,23 +150,25 @@ describe("AnalysesTable.vue", () => { }); test("Refresh table", async () => { - const wrapper = mount(AnalysisTableTestComponent); - await flushPromises(); - - const rows = wrapper.findAll("tbody tr"); - expect(rows.length).toBe(3); // Ensure 3 rows exist as defined in fakeAnalysisNodes - - // "Updated" response - fakeAnalysisNodes.push(newFakeAnalysisNode); // Add new entry to output - vi.mocked(getAnalysisNodes).mockResolvedValue({ - data: ref(fakeAnalysisNodes), + const dataRef = ref([...fakeAnalysisNodes]); + const mockRefresh = vi.fn(async () => { + dataRef.value = [...fakeAnalysisNodes, newFakeAnalysisNode]; + }); + vi.mocked(getAnalysisNodes).mockResolvedValueOnce({ + data: dataRef, pending: ref(false), error: ref(undefined), status: ref("success"), - refresh: vi.fn(), + refresh: mockRefresh, execute: vi.fn(), clear: vi.fn(), }); + + const wrapper = mount(AnalysisTableTestComponent); + await flushPromises(); + + expect(wrapper.findAll("tbody tr").length).toBe(3); + const refreshButton = wrapper.find(".table-refresh-btn"); await refreshButton.trigger("click"); await flushPromises(); @@ -255,9 +257,10 @@ describe("AnalysesTable.vue — PO calls and Progress", () => { }); // Column index of the Progress cell in each DataTable row - const PROGRESS_COL = 8; + // (0=Name, 1=Approval, 2=Build, 3=Distribution, 4=RunStatus, 5=Project, 6=DataStore, 7=Created, 8=LastUpdated, 9=Progress, 10=Controls) + const PROGRESS_COL = 9; // Column index of the Run Status cell - const RUN_STATUS_COL = 3; + const RUN_STATUS_COL = 4; test("Progress bar is indeterminate when executing and progress is 0", async () => { // fakeBaseAnalysisNode.analysis_id matches fakeAnalysisId, so PodOrc From f9309315c73b2c0b2e7e6f547c01a6cd39ac0e7d Mon Sep 17 00:00:00 2001 From: Bruce Schultz Date: Wed, 18 Mar 2026 15:27:22 +0100 Subject: [PATCH 4/4] fix(analyses): proper field for sorting distribution column --- app/components/analysis/AnalysesTable.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/analysis/AnalysesTable.vue b/app/components/analysis/AnalysesTable.vue index 86bb8d7..0c73670 100644 --- a/app/components/analysis/AnalysesTable.vue +++ b/app/components/analysis/AnalysesTable.vue @@ -633,7 +633,7 @@ const onCloseNavToast = () => {