From 1522224c4eeb1b0e6b8d1f1f4587ab02e9eedde9 Mon Sep 17 00:00:00 2001 From: tada5hi Date: Thu, 30 Jul 2026 17:29:00 +0200 Subject: [PATCH 1/2] feat!: take Hub entity types from @privateaim/core-kit and move Hub fields to camelCase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Hub renamed its entity/HTTP-API vocabulary from snake_case to camelCase in 0.13.0 (PrivateAIM/hub#1806). node-ui's copies of the Hub entities came from app/services/Api.ts, generated by swagger-typescript-api from a checked-in snapshot of the hub-adapter's OpenAPI document, so they silently went stale. Hub entities now come from the Hub's own published packages instead. The hub-adapter forwards Hub entities through unchanged — its FastAPI response_models ARE flame-hub-client's models — so the Hub, not a snapshot of the adapter's OpenAPI document, is the source of truth for their shape, and @privateaim/core-kit is versioned in lockstep with the API that produces them. - New app/services/hub.ts re-exports the eleven Hub entities plus the status enums from @privateaim/core-kit and @privateaim/kit. - app/types/analysis.ts is deleted: its ProcessStatus was a hand-copy of the Hub's, annotated "As defined by the Hub". app/types/node.ts keeps ApprovalStatus as a documented alias, because the Hub declares the same two members twice (ProjectNodeApprovalStatus / AnalysisNodeApprovalStatus) while the UI approves both kinds of node through one control. Members move to SCREAMING_CASE. - Everything the node genuinely owns — kong, pod-orchestrator, event log, container logs, node settings, health, auth — still comes from Api.ts, and keeps its snake_case fields. Only the Hub half of every mixed file moves. useDataStoreList keeps DATA_ROW_UNIX_COLS snake_case because it formats kong rows while its buildProjectNameMap reads a Hub Project; DetailedDataStoreTable is kong all the way down and is untouched; AnalysisControlButtons keeps analysis_id/project_id because that FormData goes to the adapter's own /analysis/initialize; and ApproveRejectToggle keeps its approval_status form key — that is the adapter's FastAPI Form(...) parameter, not a Hub key — while the response it reads back is a Hub entity and so becomes approvalStatus. The rename also covers everything with no compiler coverage: rapiq query strings (sort=-updatedAt, fields=id,name,displayName), PrimeVue column field/sortField/ filterField props, filter-state keys, and the composite "group::status" values the hub-status filter splits at runtime. rapiq drops an unknown sort or filter key silently rather than erroring, so a missed rename returns a wrongly-ordered or unfiltered result set with nothing in the logs. An eslint no-restricted-imports rule now fails on importing a Hub entity name from ~/services/Api, so the generated copies cannot creep back in. To make that guard actually enforce, a lint script is added and wired into CI, which required fixing six pre-existing lint errors (one unused import, one unused loop parameter, four `as any` casts) in otherwise unrelated files. BREAKING CHANGE: requires FLAME Hub 0.13.0 and a hub-adapter built against flame-hub-client with camelCase models. Against an older adapter every Hub field reads as undefined. --- .github/workflows/ci.yaml | 3 + README.md | 29 + app/components/analysis/AnalysesTable.vue | 179 +++---- .../analysis/AnalysisControlButtons.vue | 8 +- app/components/analysis/ContainerCounter.vue | 2 +- .../analysis/logs/ContainerLogs.vue | 9 +- .../create/DataStoreProjectInitializer.vue | 6 +- app/components/header/DarkModeToggle.vue | 1 - .../projects/ProjectProposalTable.vue | 36 +- app/components/table/ApproveRejectToggle.vue | 18 +- app/composables/useAPIFetch.ts | 18 +- app/composables/useDataStoreList.ts | 5 +- app/services/hub.ts | 55 ++ app/services/modifiedApiInterfaces.ts | 28 +- app/stores/hubStore.ts | 2 +- app/types/analysis.ts | 10 - app/types/node.ts | 13 +- app/utils/count-analyses.ts | 2 +- app/utils/format-data-row.ts | 8 +- app/utils/status-tag-severity.ts | 20 +- eslint.config.js | 29 + package.json | 4 + pnpm-lock.yaml | 498 +++++++++--------- pnpm-workspace.yaml | 5 + .../analysis/AnalysisControlButtons.spec.ts | 10 +- .../components/analysis/AnalysisTable.spec.ts | 64 +-- .../analysis/ContainerCounter.spec.ts | 20 +- test/components/analysis/constants.ts | 143 ++--- .../data-stores/DataStoreList.spec.ts | 3 +- test/components/data-stores/constants.ts | 79 +-- .../DataStoreProjectInitializer.spec.ts | 4 +- test/components/events/EventViewer.spec.ts | 4 +- .../projects/ProjectProposalTable.spec.ts | 2 +- test/components/projects/constants.ts | 56 +- .../table/ApproveRejectToggle.spec.ts | 12 +- test/composables/useDataStoreList.test.ts | 15 +- test/mockapi/handlers.ts | 2 +- test/utils/count-analyses.test.ts | 6 +- test/utils/format-data-row.test.ts | 42 +- 39 files changed, 788 insertions(+), 662 deletions(-) create mode 100644 app/services/hub.ts delete mode 100644 app/types/analysis.ts diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2a891e76..4808804e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -71,6 +71,9 @@ jobs: - name: Build run: pnpm run build + - name: Lint + run: pnpm run lint + - name: Test run: pnpm run test:ci diff --git a/README.md b/README.md index b4cef1cc..2999e441 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,35 @@ website, the administrators have access to an overview of the submitted projects the data at the data center, as well as have full control over the individual containers running on their systems and manage which projects/analyses have access to the various datasets. +## API Types + +Types come from two places, and the split matters. + +**FLAME Hub entities** — `Analysis`, `AnalysisNode`, `Project`, `ProjectNode`, `Node`, +`Registry`, `RegistryProject`, `MasterImage` and their status enums — are imported from +`~/services/hub`, which re-exports the Hub's own published packages +(`@privateaim/core-kit`, `@privateaim/kit`). The hub-adapter forwards Hub entities through +unchanged, so the Hub is the source of truth for their shape, and its packages are versioned +in lockstep with the API that produces them. + +**Everything the node owns** — kong, the pod-orchestrator, the event log, container logs, +node settings, health and auth — comes from `app/services/Api.ts`, generated from the +hub-adapter's OpenAPI document: + +```bash +pnpm build-api +``` + +That document also carries the adapter's own copies of the Hub entities. They are still +generated but must not be used: an ESLint `no-restricted-imports` rule fails the lint if a +Hub entity name is imported from `~/services/Api`, because those copies drift from the Hub +whenever the adapter lags behind a release. + +Hub fields are **camelCase** (`displayName`, `createdAt`, `buildStatus`) as of Hub 0.13.0. +Node-local fields are still snake_case (`created_at` on a kong route, `event_name`, +`start_date`). The two look alike and sit side by side in the same components — check which +service owns a field before renaming it. + ## Required Environment Variables This frontend requires the following environment variables to be set. The SSR framework used for this project (Nuxt) diff --git a/app/components/analysis/AnalysesTable.vue b/app/components/analysis/AnalysesTable.vue index 216c1c7b..785d83ac 100644 --- a/app/components/analysis/AnalysesTable.vue +++ b/app/components/analysis/AnalysesTable.vue @@ -18,14 +18,13 @@ import { getExecutionStatusSeverity, } from "~/utils/status-tag-severity"; import { - type AnalysisNode, type AnalysisStatus, type ListRoutes, type PodProgressResponse, PodStatus, - type Project, type Route, } from "~/services/Api"; +import { type AnalysisNode, ProcessStatus, type Project } from "~/services/hub"; import { ApprovalStatus } from "~/types/node"; import { parseKongTags } from "~/utils/parse-kong-tags"; import ContainerCounter from "~/components/analysis/ContainerCounter.vue"; @@ -34,7 +33,6 @@ import type { HubStatuses, ModifiedAnalysisNode, } from "~/services/modifiedApiInterfaces"; -import { ProcessStatus } from "~/types/analysis"; const toast = useToast(); const tableLoading = ref(true); @@ -87,13 +85,10 @@ const podStatuses = Object.values(PodStatus); // Combined "Hub Statuses" column filtering const HUB_STATUS_FILTER_MATCH_MODE = "hubStatusMatch"; -type HubStatusGroup = - | "approval_status" - | "build_status" - | "distribution_status"; +type HubStatusGroup = "approvalStatus" | "buildStatus" | "distributionStatus"; function getHubStatusSeverity(group: HubStatusGroup, status: string) { - return group === "approval_status" + return group === "approvalStatus" ? getApprovalStatusSeverity(status as ApprovalStatus) : getBuildStatusSeverity(status as ProcessStatus); } @@ -103,8 +98,8 @@ const hubStatusFilterOptions = [ label: "Approval Status", items: approvalStatuses.map((status) => ({ label: status, - value: `approval_status::${status}`, - group: "approval_status" as HubStatusGroup, + value: `approvalStatus::${status}`, + group: "approvalStatus" as HubStatusGroup, status, })), }, @@ -112,8 +107,8 @@ const hubStatusFilterOptions = [ label: "Build Status", items: processStatuses.map((status) => ({ label: status, - value: `build_status::${status}`, - group: "build_status" as HubStatusGroup, + value: `buildStatus::${status}`, + group: "buildStatus" as HubStatusGroup, status, })), }, @@ -121,8 +116,8 @@ const hubStatusFilterOptions = [ label: "Distribution Status", items: processStatuses.map((status) => ({ label: status, - value: `distribution_status::${status}`, - group: "distribution_status" as HubStatusGroup, + value: `distributionStatus::${status}`, + group: "distributionStatus" as HubStatusGroup, status, })), }, @@ -148,8 +143,8 @@ async function getProjects() { .$hubApi("/projects", { method: "GET", query: { - sort: "-updated_at", - fields: "id,name,display_name", + sort: "-updatedAt", + fields: "id,name,displayName", }, }) .catch(() => undefined)) as Project[]; @@ -187,7 +182,7 @@ async function parseProjects() { if (projData) { projData.forEach((proj: Project) => { if (proj.id) { - projMap.set(proj.id, proj.display_name ?? proj.name ?? proj.id); + projMap.set(proj.id, proj.displayName ?? proj.name ?? proj.id); } }); } @@ -219,16 +214,16 @@ async function getExecutionStatusesFromPodOrc(): Promise< function setProgress(analysis: ModifiedAnalysisNode): ModifiedAnalysisNode { // For testing: Math.round(Math.random() * 100); - analysis.execution_progress = analysis.execution_progress - ? analysis.execution_progress + analysis.executionProgress = analysis.executionProgress + ? analysis.executionProgress : 0; - const currentRunStatus = analysis.execution_status; + const currentRunStatus = analysis.executionStatus; if (currentRunStatus) { if (currentRunStatus === PodStatus.Failed) { - analysis.execution_progress = 0; + analysis.executionProgress = 0; } else if (currentRunStatus === PodStatus.Executed) { - analysis.execution_progress = 100; + analysis.executionProgress = 100; } } return analysis; @@ -256,14 +251,14 @@ function parseAnalysis( analysisEntry: ModifiedAnalysisNode, executionStatuses: PodProgressResponse | undefined, ): ModifiedAnalysisNode { - const projId = analysisEntry.analysis?.project_id; - const analysisId = analysisEntry.analysis_id; - analysisEntry.analysis_name = - analysisEntry.analysis?.display_name ?? + const projId = analysisEntry.analysis?.projectId; + const analysisId = analysisEntry.analysisId; + analysisEntry.analysisName = + analysisEntry.analysis?.displayName ?? analysisEntry.analysis?.name ?? analysisId; if (projId) { - analysisEntry.project_name = projMap.has(projId) ? projMap.get(projId) : ""; + analysisEntry.projectName = projMap.has(projId) ? projMap.get(projId) : ""; analysisEntry.datastore = kongRoutes.value.has(projId); } const acceptableHubStatuses: Array = [ @@ -272,18 +267,18 @@ function parseAnalysis( ]; if (executionStatuses && analysisId in executionStatuses) { const podStatus = executionStatuses[analysisId]!; - analysisEntry.execution_status = podStatus.status; - analysisEntry.execution_progress = - podStatus.progress ?? analysisEntry.execution_progress; + analysisEntry.executionStatus = podStatus.status; + analysisEntry.executionProgress = + podStatus.progress ?? analysisEntry.executionProgress; } else { - if (!acceptableHubStatuses.includes(analysisEntry.execution_status)) { - analysisEntry.execution_status = null; + if (!acceptableHubStatuses.includes(analysisEntry.executionStatus)) { + analysisEntry.executionStatus = null; } } - analysisEntry.hub_statuses = { - approval_status: analysisEntry.approval_status, - build_status: analysisEntry.analysis?.build_status ?? null, - distribution_status: analysisEntry.analysis?.distribution_status ?? null, + analysisEntry.hubStatuses = { + approvalStatus: analysisEntry.approvalStatus, + buildStatus: analysisEntry.analysis?.buildStatus ?? null, + distributionStatus: analysisEntry.analysis?.distributionStatus ?? null, }; return setProgress(analysisEntry); } @@ -314,13 +309,13 @@ async function compileAnalysisTable( const formattedAnalyses = formatDataRow( analysisData, - ["created_at", "updated_at"], + ["createdAt", "updatedAt"], expandRowEntries, ) as ModifiedAnalysisNode[]; if (formattedAnalyses && projMap.size > 0) { formattedAnalyses.forEach((analysisEntry: ModifiedAnalysisNode) => { parsedAnalyses.set( - analysisEntry.analysis_id, + analysisEntry.analysisId, parseAnalysis(analysisEntry, currentExecutionStatuses), ); }); @@ -385,7 +380,7 @@ async function getNextPage() { limit: queryLimit, }, include: "analysis,node", - sort: "-updated_at", + sort: "-updatedAt", }, }) .catch(() => undefined)) as AnalysisNode[] | undefined; @@ -405,8 +400,8 @@ async function getNextPage() { // Table filters const defaultFilters = { global: { value: undefined, matchMode: FilterMatchMode.CONTAINS }, - hub_statuses: { value: undefined, matchMode: HUB_STATUS_FILTER_MATCH_MODE }, - execution_status: { value: undefined, matchMode: FilterMatchMode.IN }, + hubStatuses: { value: undefined, matchMode: HUB_STATUS_FILTER_MATCH_MODE }, + executionStatus: { value: undefined, matchMode: FilterMatchMode.IN }, }; filters.value = defaultFilters; @@ -432,18 +427,18 @@ function updateAnalysisRun( if (analysesMap.value.has(analysisId)) { const analysisToUpdate = analysesMap.value.get(analysisId)!; // Tell typescript we are sure there is a value if (newStatusData) { - analysisToUpdate.execution_status = newStatusData.status; - analysisToUpdate.execution_progress = newStatusData.progress || 0; + analysisToUpdate.executionStatus = newStatusData.status; + analysisToUpdate.executionProgress = newStatusData.progress || 0; } analysesMap.value.set(analysisId, setProgress(analysisToUpdate)); } } function updateExecutionStatusFilter(filterText: string) { - const currentExecutionStatusFilters = filters.value.execution_status.value; + const currentExecutionStatusFilters = filters.value.executionStatus.value; if (!currentExecutionStatusFilters) { // If value is null then initialize with filter in array - filters.value.execution_status.value = [filterText]; + filters.value.executionStatus.value = [filterText]; } else { // Already run status filters present if (currentExecutionStatusFilters.includes(filterText)) { @@ -453,13 +448,13 @@ function updateExecutionStatusFilter(filterText: string) { ); if (filteredStatuses.length == 0) { // If empty array after filtering then set to null - filters.value.execution_status.value = undefined; + filters.value.executionStatus.value = undefined; } else { - filters.value.execution_status.value = filteredStatuses; + filters.value.executionStatus.value = filteredStatuses; } } else { // Apply filter since it isn't present - filters.value.execution_status.value.push(filterText); + filters.value.executionStatus.value.push(filterText); } } } @@ -563,7 +558,7 @@ const onCloseNavToast = () => { { dataKey="id" filterDisplay="menu" paginator - sortField="updated_at.timestamp" + sortField="updatedAt.timestamp" stripedRows tableStyle="min-width: 50rem" @page="onPage" > - + @@ -598,8 +593,8 @@ const onCloseNavToast = () => { :showClearButton="false" :showFilterMatchModes="false" :showFilterOperator="false" - field="hub_statuses" - filterField="hub_statuses" + field="hubStatuses" + filterField="hubStatuses" > - + - + @@ -822,18 +817,18 @@ const onCloseNavToast = () => { @@ -849,17 +844,17 @@ const onCloseNavToast = () => {