Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions components/analysis/AnalysesTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,26 @@ import {
import { AnalysisBuildStatus, AnalysisNodeRunStatus } from "~/types/analysis";
import { ApprovalStatus } from "~/types/node";
import ContainerCounter from "~/components/analysis/ContainerCounter.vue";
import { useNodeType } from "~/composables/useNodeType";
import { useDatastoreRequirement } from "~/composables/useDatastoreRequirement";
import type { ModifiedAnalysisNode } from "~/services/modifiedApiInterfaces";

const toast = useToast();
const nodeType = await useNodeType();

// Data Store Requirement Check
const { datastoreState } = await useDatastoreRequirement();
const datastoreRequired = computed(
() => datastoreState.value.datastoreRequired,
);
const nodeType = computed(() => datastoreState.value.nodeType);

const datastoreBadgeSeverity = computed(() =>
datastoreRequired.value ? "danger" : "secondary",
);
const datastoreBadgeTooltip = computed(() =>
datastoreRequired.value
? "Data store missing!"
: "Data store missing, but not required",
);
Comment on lines +36 to +50

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle unknown datastoreRequired explicitly.

If useDatastoreRequirement yields null (e.g., failed fetch), the current logic treats it as false, enabling controls and showing the “not required” tooltip. Consider defaulting to a safe value or surfacing an “unknown” state, and remove the non-null assertion when passing the prop.

🛡️ Safer fallback example
-const datastoreRequired = computed(
-  () => datastoreState.value.datastoreRequired,
-);
+const datastoreRequired = computed(
+  () => datastoreState.value.datastoreRequired ?? true,
+);

-const datastoreBadgeTooltip = computed(() =>
-  datastoreRequired.value
-    ? "Data store missing!"
-    : "Data store missing, but not required",
-);
+const datastoreBadgeTooltip = computed(() =>
+  datastoreState.value.datastoreRequired === null
+    ? "Datastore requirement unknown"
+    : datastoreRequired.value
+      ? "Data store missing!"
+      : "Data store missing, but not required",
+);
-:requireDatastore="datastoreRequired!"
+:requireDatastore="datastoreRequired"

Also applies to: 659-664, 745-746

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/analysis/AnalysesTable.vue` around lines 36 - 50, The current
logic assumes useDatastoreRequirement() always returns a valid datastoreState
and treats null as false; update the code that derives
datastoreState/datastoreRequired/nodeType to explicitly handle a null result by
introducing an "unknown" fallback (e.g., datastoreState === null ->
datastoreRequired = null or 'unknown') and update computed properties
datastoreBadgeSeverity and datastoreBadgeTooltip to branch on the unknown case
(e.g., show neutral/severity "warning" and tooltip "Data store status unknown")
instead of defaulting to not-required; also remove any non-null assertions where
the prop is passed so the component receives the explicit null/unknown value and
can render a safe disabled state. Ensure these changes are applied to the same
patterns around datastoreState/datastoreRequired/nodeType usage (including the
other occurrences noted).


const analysesMap = ref<Map<string, ModifiedAnalysisNode>>(new Map());
const analyses = computed(() => Array.from(analysesMap.value.values()));
Expand Down Expand Up @@ -641,9 +656,11 @@ const onCloseNavToast = () => {
></Badge>
</div>
<div v-else class="datastore-badge">
<Badge class="w-8 h-8 rounded-full" severity="danger"
<Badge
class="w-8 h-8 rounded-full"
:severity="datastoreBadgeSeverity"
><i
v-tooltip.top="'Data store missing!'"
v-tooltip.top="datastoreBadgeTooltip"
class="pi pi-times"
></i
></Badge>
Expand Down Expand Up @@ -725,7 +742,7 @@ const onCloseNavToast = () => {
:analysisRunStatus="slotProps.data.run_status"
:datastore="slotProps.data.datastore"
:nodeId="slotProps.data.node_id"
:nodeType="nodeType!"
:requireDatastore="datastoreRequired!"
:projectId="slotProps.data.analysis.project_id"
@missingDataStore="showDataStoreNavToast"
@updateAnalysisRow="updateAnalysisRun"
Expand Down
12 changes: 6 additions & 6 deletions components/analysis/AnalysisControlButtons.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ const props = defineProps({
type: Boolean,
required: true,
},
nodeType: {
type: String,
requireDatastore: {
type: Boolean,
required: true,
},
Comment on lines +43 to 46

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n --type=vue "<AnalysisControlButtons" -C3

Repository: PrivateAIM/node-ui

Length of output: 89


🏁 Script executed:

fd -e vue -x grep -l "AnalysisControlButtons" {} + | head -20

Repository: PrivateAIM/node-ui

Length of output: 1413


🏁 Script executed:

rg -n "AnalysisControlButtons" --type-list | head -5

Repository: PrivateAIM/node-ui

Length of output: 451


🏁 Script executed:

rg -n "AnalysisControlButtons" -C3 --no-heading | head -100

Repository: PrivateAIM/node-ui

Length of output: 5034


🏁 Script executed:

sed -n '735,755p' components/analysis/AnalysesTable.vue

Repository: PrivateAIM/node-ui

Length of output: 964


🏁 Script executed:

grep -n "requireDatastore" components/analysis/AnalysesTable.vue

Repository: PrivateAIM/node-ui

Length of output: 122


🏁 Script executed:

grep -n "requireDatastore" test/components/analysis/AnalysisControlButtons.spec.ts | head -20

Repository: PrivateAIM/node-ui

Length of output: 44


🏁 Script executed:

sed -n '53,64p' test/components/analysis/AnalysisControlButtons.spec.ts

Repository: PrivateAIM/node-ui

Length of output: 514


🏁 Script executed:

sed -n '277,288p' test/components/analysis/AnalysisControlButtons.spec.ts

Repository: PrivateAIM/node-ui

Length of output: 506


Update test mount calls to include the required requireDatastore prop.

The production usage in AnalysesTable.vue correctly passes :requireDatastore="datastoreRequired!". However, the test file at test/components/analysis/AnalysisControlButtons.spec.ts has two mount calls (lines 53-64 and 277-288) that do not include this required prop, which will cause runtime warnings or test failures.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/analysis/AnalysisControlButtons.vue` around lines 43 - 46, Tests
mounting AnalysisControlButtons are missing the required prop requireDatastore
which causes warnings/failures; update both mount calls in
test/components/analysis/AnalysisControlButtons.spec.ts that instantiate the
AnalysisControlButtons component to pass the prop (e.g., requireDatastore: true
or use the same test fixture value as production like
:requireDatastore="datastoreRequired" or datastoreRequired!) so the mounted
wrapper includes the required Boolean prop; locate the mounts that call
mount(AnalysisControlButtons, { props: {...} }) and add requireDatastore to the
props object.

});
Expand Down Expand Up @@ -274,7 +274,7 @@ async function onDeleteAnalysis() {
:disabled="
!buttonStatuses.playActive ||
!(props.analysisBuildStatus === AnalysisBuildStatus.Finished) ||
(!props.datastore && props.nodeType != 'aggregator')
(!props.datastore && props.requireDatastore)
"
:loading="loading"
aria-label="Start"
Expand All @@ -289,7 +289,7 @@ async function onDeleteAnalysis() {
:disabled="
!buttonStatuses.rerunActive ||
!(props.analysisBuildStatus === AnalysisBuildStatus.Finished) ||
(!props.datastore && props.nodeType != 'aggregator')
(!props.datastore && props.requireDatastore)
"
:loading="loading"
aria-label="Rerun"
Expand All @@ -303,7 +303,7 @@ async function onDeleteAnalysis() {
:disabled="
!buttonStatuses.stopActive ||
!(props.analysisBuildStatus === AnalysisBuildStatus.Finished) ||
(!props.datastore && props.nodeType != 'aggregator')
(!props.datastore && props.requireDatastore)
"
:loading="loading"
aria-label="Stop"
Expand All @@ -317,7 +317,7 @@ async function onDeleteAnalysis() {
:disabled="
!buttonStatuses.deleteActive ||
!(props.analysisBuildStatus === AnalysisBuildStatus.Finished) ||
(!props.datastore && props.nodeType != 'aggregator')
(!props.datastore && props.requireDatastore)
"
:loading="loading"
aria-label="Delete"
Expand Down
36 changes: 20 additions & 16 deletions components/events/EventViewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import SearchBar from "~/components/table/SearchBar.vue";
import {
EventLogLevelTag,
EventServiceTag,
type EventTag
type EventTag,
} from "~/types/eventTag";
import TagFilterSidePanel from "~/components/events/TagFilterSidePanel.vue";
import type { EventLog, EventLogResponse } from "~/services/Api";
Expand All @@ -21,21 +21,21 @@ const appliedFilters = ref<EventTag[]>([]);
const logLevelColorMap = new Map([
[EventLogLevelTag.Error, "#ef4444"],
[EventLogLevelTag.Warning, "#eab308"],
[EventLogLevelTag.Info, "#3b82f6"]
[EventLogLevelTag.Info, "#3b82f6"],
]);
const logLevelDistributions = ref(
Array.from(logLevelColorMap, ([label, color]) => ({
label,
color,
value: 0
}))
value: 0,
})),
);

const filters = ref();

const dateTimeFormat = new Intl.DateTimeFormat(undefined, {
dateStyle: "short",
timeStyle: "long"
timeStyle: "long",
});

const { data: response, status } = await getEvents();
Expand Down Expand Up @@ -87,12 +87,16 @@ function formatEventName(eventName: string): string {
return eventChunks.join("-");
}

function formatTimestamp(timestamp: string): string {
const date = new Date(timestamp + "Z"); // Python does not return timestamp with standard "Z"
const formattedDateTime = dateTimeFormat.format(date);
const [dateString, timeString] = formattedDateTime.split(", ");
function formatTimestamp(timestamp: string): string | undefined {
try {
const date = new Date(timestamp); // Python does not return timestamp with standard "Z"
const formattedDateTime = dateTimeFormat.format(date);
const [dateString, timeString] = formattedDateTime.split(", ");

return `${dateString}<br><b>${timeString}</b>`;
return `${dateString}<br><b>${timeString}</b>`;
} catch (error) {
console.error(`Timestamp: ${timestamp}; Error: ${error}`);
}
}

function getLogLevelColor(tags: string[]): string | undefined {
Expand All @@ -101,7 +105,7 @@ function getLogLevelColor(tags: string[]): string | undefined {
for (const tag of [
EventLogLevelTag.Error,
EventLogLevelTag.Warning,
EventLogLevelTag.Info
EventLogLevelTag.Info,
]) {
if (tags.includes(tag)) {
return logLevelColorMap.get(tag);
Expand All @@ -127,7 +131,7 @@ FilterService.register("tagsContainsAny", (value, filter) => {

const defaultFilters = {
global: { value: null, matchMode: FilterMatchMode.CONTAINS },
"attributes.tags": { value: null, matchMode: "tagsContainsAny" }
"attributes.tags": { value: null, matchMode: "tagsContainsAny" },
};

filters.value = defaultFilters;
Expand All @@ -140,7 +144,7 @@ function clearAllFilters() {
const clearedFilters = {};
for (const filterKey in defaultFilters) {
clearedFilters[filterKey] = {
...defaultFilters[filterKey]
...defaultFilters[filterKey],
};
clearedFilters[filterKey].value = null;
}
Expand All @@ -150,7 +154,7 @@ function clearAllFilters() {

function handleRemoveFilterTag(tag: EventTag) {
appliedFilters.value = appliedFilters.value.filter(
(filter) => filter !== tag
(filter) => filter !== tag,
);
}

Expand All @@ -175,7 +179,7 @@ watch(
filters.value["attributes.tags"].value = null;
}
},
{ deep: true }
{ deep: true },
);
</script>

Expand All @@ -196,7 +200,7 @@ watch(
<div class="table-header-row-filter-chips-container">
<div class="table-header-row-filter-chips-container-counter">
<span
><b>FILTERS: ({{ appliedFilters.length }})</b></span
><b>FILTERS: ({{ appliedFilters.length }})</b></span
>
</div>
<div class="table-header-row-filter-chips flex flex-wrap gap-2">
Expand Down
29 changes: 29 additions & 0 deletions components/header/AvatarButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,18 @@ import Menu from "primevue/menu";
import Button from "primevue/button";
import { useRuntimeConfig } from "#app";
import CleanupDialog from "~/components/header/CleanupDialog.vue";
import { useDatastoreRequirement } from "~/composables/useDatastoreRequirement";

const { signIn, signOut, status, data } = useAuth();

const menu = ref();

const { datastoreState, setDatastoreRequired } =
await useDatastoreRequirement();
const datastoreRequired = computed(
() => datastoreState.value.datastoreRequired,
);

const config = useRuntimeConfig();
const baseUrl = new URL(config.public.baseUrl).origin;
const idpProvider = config.public.idpProvider;
Expand All @@ -28,6 +35,7 @@ const menuItems = ref([
label: userActionLabel,
icon: userActionIcon,
command: () => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
isAuthenticated.value ? signOut() : signIn(`${idpProvider}`);
},
},
Expand All @@ -45,6 +53,11 @@ const menuItems = ref([
},
disabled: !isAuthenticated.value,
},
{
label: "Require Data Store",
icon: "pi pi-database",
isToggle: true, // custom flag to identify it in the slot
},
],
},
]);
Expand Down Expand Up @@ -92,6 +105,22 @@ const toggle = (event) => {
>
<template #item="{ item, props }">
<a
v-if="item.isToggle"
v-ripple
class="flex items-center"
v-bind="props.action"
@click.prevent="setDatastoreRequired(!datastoreRequired)"
>
<i :class="item.icon" />
<span class="ml-2 menu-item-label">{{ item.label }}</span>
<ToggleSwitch
:modelValue="datastoreRequired ?? false"
class="ml-auto"
@click.stop="setDatastoreRequired(!datastoreRequired)"
/>
</a>
<a
v-else
v-ripple
:href="item.url"
:target="item.target"
Expand Down
9 changes: 9 additions & 0 deletions composables/useAPIFetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
ListConsumers,
ListRoutes,
ListServices,
NodeSettings,
Project,
ProjectNode,
Service,
Expand All @@ -32,6 +33,14 @@ export function getEvents(opts?) {
});
}

// Node endpoints
export function getNodeConfiguration(opts?) {
return useAPIFetch<NodeSettings>("/node/settings", {
...opts,
method: "GET",
});
}
Comment on lines +36 to +42

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n 'getNodeConfiguration' --type=ts --type=vue

Repository: PrivateAIM/node-ui

Length of output: 89


🏁 Script executed:

rg -n 'getNodeConfiguration' --type ts

Repository: PrivateAIM/node-ui

Length of output: 137


🏁 Script executed:

rg -n 'useDatastoreRequirement' --type ts | head -20

Repository: PrivateAIM/node-ui

Length of output: 1005


🏁 Script executed:

cat composables/useDatastoreRequirement.ts

Repository: PrivateAIM/node-ui

Length of output: 1724


Remove getNodeConfiguration or refactor useDatastoreRequirement to use it.

getNodeConfiguration is unused throughout the codebase. useDatastoreRequirement directly calls useNuxtApp().$hubApi("/node/settings", ...) instead of using this helper function. Either remove the unused function or refactor the composable to use it for consistency with other endpoints.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@composables/useAPIFetch.ts` around lines 36 - 42, getNodeConfiguration is
defined but unused; update useDatastoreRequirement to call
getNodeConfiguration(...) instead of directly invoking
useNuxtApp().$hubApi("/node/settings", ...), passing through the same opts and
handling the returned Promise/useAPIFetch result (NodeSettings) so behavior and
typing remain identical; alternatively, if you prefer not to change
useDatastoreRequirement, remove the unused getNodeConfiguration export to avoid
dead code.


// Hub endpoints
export function getProjectNodes(opts?) {
return useAPIFetch<ProjectNode[]>("/project-nodes", {
Expand Down
58 changes: 58 additions & 0 deletions composables/useDatastoreRequirement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { type NodeTypeResponse } from "~/services/Api";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Missing NodeSettings import — will cause a TypeScript error.

NodeSettings is used as a type assertion on lines 21 and 47 (as NodeSettings), but only NodeTypeResponse is imported.

🐛 Proposed fix
-import { type NodeTypeResponse } from "~/services/Api";
+import { type NodeSettings, type NodeTypeResponse } from "~/services/Api";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { type NodeTypeResponse } from "~/services/Api";
import { type NodeSettings, type NodeTypeResponse } from "~/services/Api";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@composables/useDatastoreRequirement.ts` at line 1, The file is missing the
NodeSettings type import used in the type assertions at lines where "as
NodeSettings" appears; update the import line that currently imports
NodeTypeResponse to also import NodeSettings from the same module (e.g., add
NodeSettings to the import from "~/services/Api") so the type assertions in
useDatastoreRequirement (references to NodeSettings) compile without TypeScript
errors.

import { useNuxtApp, useState } from "#app";

interface DatastoreState {
datastoreRequired: boolean | null;
nodeType: string | null;
}

export async function useDatastoreRequirement() {
const datastoreState = useState<DatastoreState>("datastoreRequired", () => ({
datastoreRequired: null,
nodeType: null,
}));

let dataRequired: boolean = true;

// Get node configuration settings
if (!datastoreState.value.datastoreRequired) {
const nodeConfigResp = (await useNuxtApp()
.$hubApi("/node/settings", { method: "GET" })
.catch(() => null)) as NodeSettings;
if (nodeConfigResp) {
dataRequired = Boolean(nodeConfigResp.data_required);
}
}
Comment on lines +18 to +25

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Falsy check causes re-fetching when datastoreRequired is false.

!datastoreState.value.datastoreRequired is true when datastoreRequired is either null (uninitialized) or false (explicitly not required). This means every call to the composable when the datastore is not required will re-fetch /node/settings, defeating the caching purpose of useState.

The same issue exists on line 27 with !datastoreState.value.nodeType, although nodeType is a string so it's only problematic if the string can be empty.

🐛 Proposed fix — use strict null checks
-  if (!datastoreState.value.datastoreRequired) {
+  if (datastoreState.value.datastoreRequired === null) {
-  if (!datastoreState.value.nodeType) {
+  if (datastoreState.value.nodeType === null) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!datastoreState.value.datastoreRequired) {
const nodeConfigResp = (await useNuxtApp()
.$hubApi("/node/settings", { method: "GET" })
.catch(() => null)) as NodeSettings;
if (nodeConfigResp) {
dataRequired = Boolean(nodeConfigResp.data_required);
}
}
if (datastoreState.value.datastoreRequired === null) {
const nodeConfigResp = (await useNuxtApp()
.$hubApi("/node/settings", { method: "GET" })
.catch(() => null)) as NodeSettings;
if (nodeConfigResp) {
dataRequired = Boolean(nodeConfigResp.data_required);
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@composables/useDatastoreRequirement.ts` around lines 18 - 25, The falsy
checks in the composable (involving datastoreState.value.datastoreRequired and
datastoreState.value.nodeType) cause unnecessary re-fetches because they treat
explicit false (and potentially empty string) as uninitialized; change the
guards in useDatastoreRequirement to check for null/undefined explicitly (e.g.,
datastoreState.value.datastoreRequired == null or
datastoreState.value.datastoreRequired === undefined) before calling
useNuxtApp().$hubApi("/node/settings"), and for nodeType check only for
null/undefined (or if empty string is meaningful, handle that case explicitly)
so that an explicit false or valid empty string does not trigger the fetch; keep
the existing nodeConfigResp handling (nodeConfigResp.data_required) but only
execute it when the state was truly uninitialized.


if (!datastoreState.value.nodeType) {
// Re-fetch if node type couldn't be obtained previously
const nodeResp = (await useNuxtApp()
.$hubApi("/node-type", { method: "GET" })
.catch(() => null)) as NodeTypeResponse;

if (nodeResp) {
datastoreState.value = {
nodeType: nodeResp.type,
datastoreRequired: nodeResp.type !== "aggregator" && dataRequired,
};
}
}

async function setDatastoreRequired(updatedRequirement: boolean) {
const nodeConfigResp = (await useNuxtApp()
.$hubApi("/node/settings", {
method: "POST",
body: { data_required: updatedRequirement },
})
.catch(() => null)) as NodeSettings;

if (nodeConfigResp) {
datastoreState.value = {
...datastoreState.value,
datastoreRequired: nodeConfigResp.data_required,
};
}
}
Comment on lines +41 to +55

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

setDatastoreRequired ignores the aggregator-type guard.

The initial fetch on line 36 applies nodeResp.type !== "aggregator" && dataRequired, but setDatastoreRequired writes nodeConfigResp.data_required directly without the aggregator check. If an aggregator node toggles this on, the UI will show datastoreRequired = true even though aggregator nodes should never require a datastore.

🐛 Proposed fix
     if (nodeConfigResp) {
       datastoreState.value = {
         ...datastoreState.value,
-        datastoreRequired: nodeConfigResp.data_required,
+        datastoreRequired:
+          datastoreState.value.nodeType !== "aggregator" &&
+          Boolean(nodeConfigResp.data_required),
       };
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@composables/useDatastoreRequirement.ts` around lines 41 - 55, The setter
setDatastoreRequired currently writes nodeConfigResp.data_required directly and
ignores the aggregator-type rule; update setDatastoreRequired to inspect
nodeConfigResp.type and only set datastoreState.value.datastoreRequired to
nodeConfigResp.data_required when nodeConfigResp.type !== "aggregator"
(otherwise set it to false), referencing the existing symbols
setDatastoreRequired, nodeConfigResp and datastoreState to locate the code path
to change.


return { datastoreState, setDatastoreRequired };
}
21 changes: 0 additions & 21 deletions composables/useNodeType.ts

This file was deleted.

Loading