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
1 change: 1 addition & 0 deletions components/events/DateFilterGraph.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { useNuxtApp } from "#app";
import DatePicker from "primevue/datepicker";
import Toolbar from "primevue/toolbar";
import FloatLabel from "primevue/floatlabel";
Expand Down
58 changes: 39 additions & 19 deletions components/events/EventViewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import { getEvents } from "~/composables/useAPIFetch";
import { FilterMatchMode, FilterService } from "@primevue/core/api";
import SearchBar from "~/components/table/SearchBar.vue";
import MeterGroup from "primevue/metergroup";
import Chip from "primevue/chip";
import {
EventLogLevelTag,
EventServiceTag,
Expand Down Expand Up @@ -52,21 +54,27 @@ onMounted(() => {
parseData();
});

function updateLogLevelCounts() {
// Reset counts
logLevelDistributions.value.forEach((dist) => (dist.value = 0));
function updateLogLevelCounts(eventList: EventLog[] = events.value) {
// Reset counts with new array to force MeterGroup reactivity
const updatedCounts = new Map(
logLevelDistributions.value.map((dist) => [
dist.label,
{ ...dist, value: 0 },
]),
);

// Count occurrences
if (events.value) {
events.value.forEach((event) => {
if (eventList) {
// Count occurrences
eventList.forEach((event) => {
const tags = event.attributes?.tags || [];

logLevelDistributions.value.forEach((dist) => {
updatedCounts.forEach((dist) => {
if (tags.includes(dist.label)) {
dist.value++;
}
});
});
logLevelDistributions.value = Array.from(updatedCounts.values());
}
}

Expand Down Expand Up @@ -117,16 +125,25 @@ function getLogLevelColor(tags: string[]): string | undefined {

// Table filters
FilterService.register("tagsContainsAny", (value, filter) => {
if (!filter || filter.length === 0) {
return true;
}
if (!filter || filter.length === 0) return true;
if (!value || value.length === 0) return false;

if (!value || value.length === 0) {
return false;
}
const selectedServices = filter.filter((f: EventTag) =>
Object.values(EventServiceTag).includes(f),
);
const selectedLogLevels = filter.filter((f: EventTag) =>
Object.values(EventLogLevelTag).includes(f),
);

const matchesService =
selectedServices.length === 0 ||
selectedServices.some((f: EventTag) => value.includes(f));

// Check if any element in the filter array exists in the value array
return filter.some((filterItem: EventTag) => value.includes(filterItem));
const matchesLogLevel =
selectedLogLevels.length === 0 ||
selectedLogLevels.some((f: EventTag) => value.includes(f));

return matchesService && matchesLogLevel;
});

const defaultFilters = {
Expand Down Expand Up @@ -165,9 +182,9 @@ function handleShowRequestEvents(requestedEvents: EventLogResponse) {
}

function handleFilter(event: DataTableFilterEvent) {
filteredEventCount.value = event.filteredValue
? event.filteredValue.length
: 0;
const filtered = event.filteredValue ?? [];
filteredEventCount.value = filtered.length;
updateLogLevelCounts(filtered);
}

watch(
Expand All @@ -194,7 +211,10 @@ watch(
/>
</div>
<div class="log-distribution-meter">
<MeterGroup :value="logLevelDistributions" :max="events.length" />
<MeterGroup
:value="logLevelDistributions"
:max="filteredEventCount || eventCount"
/>
</div>
<div class="table-header-row">
<div class="table-header-row-filter-chips-container">
Expand Down
1 change: 1 addition & 0 deletions components/events/TagFilterSidePanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from "~/types/eventTag";
import Panel from "primevue/panel";
import PanelMenu from "primevue/panelmenu";
import Checkbox from "primevue/checkbox";

const modelValue = defineModel<EventTag[]>({ default: [] });
const emit = defineEmits(["clearTagFilter"]);
Expand Down
1 change: 1 addition & 0 deletions components/header/AvatarButton.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script lang="ts" setup>
import Menu from "primevue/menu";
import ToggleSwitch from "primevue/toggleswitch";
import Button from "primevue/button";
import { useRuntimeConfig } from "#app";
import CleanupDialog from "~/components/header/CleanupDialog.vue";
Expand Down
3 changes: 3 additions & 0 deletions composables/useAPIFetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export function getEvents(opts?) {
return useAPIFetch<EventLogResponse>("/events", {
...opts,
method: "GET",
query: {
limit: 100,
},
});
}

Expand Down
4 changes: 2 additions & 2 deletions test/components/analysis/AnalysisControlButtons.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ describe("AnalysisControlButtons.vue", () => {
projectId: "7f2f3b59-3b6d-4fb6-a900-2a4d5c2ea483",
nodeId: "e3b89572-327f-4936-8cf0-fbfbcc6336b7",
datastore: true,
nodeType: "default",
requireDatastore: true,
},
});

Expand Down Expand Up @@ -283,7 +283,7 @@ describe("AnalysisControlButtons.vue", () => {
projectId: "7f2f3b59-3b6d-4fb6-a900-2a4d5c2ea483",
nodeId: "e3b89572-327f-4936-8cf0-fbfbcc6336b7",
datastore: true,
nodeType: "default",
requireDatastore: true,
},
});

Expand Down
40 changes: 40 additions & 0 deletions test/components/events/DateFilterGraph.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { describe, expect, it, vi } from "vitest";
import { flushPromises, mount } from "@vue/test-utils";
import DateFilterGraph from "~/components/events/DateFilterGraph.vue";
import { fakeEventResponse } from "~/test/components/events/constants";

vi.mock("#app", () => ({
useNuxtApp: () => ({
$hubApi: vi.fn().mockResolvedValue(fakeEventResponse),
}),
}));

describe("DateFilterGraph.vue", () => {
it("Show event count", () => {
const wrapper = mount(DateFilterGraph, {
props: { eventCount: 2 },
});

expect(wrapper.text()).toContain("Event Viewer");
expect(wrapper.html()).toContain("2 events");
});

it("Shows singular form when eventCount is 1", () => {
const wrapper = mount(DateFilterGraph, {
props: { eventCount: 1 },
});

expect(wrapper.html()).toContain("1 event");
});

it("Emits showRequestedEvents when submit clicked", async () => {
const wrapper = mount(DateFilterGraph, { props: { eventCount: 1 } });

const submit = wrapper.find(".custom-date-filter-submit-btn button");
expect(submit.exists()).toBe(true);
await submit.trigger("click");
await flushPromises();

expect(wrapper.emitted("showRequestedEvents")).toHaveLength(1);
});
});
84 changes: 84 additions & 0 deletions test/components/events/EventViewer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { flushPromises, mount } from "@vue/test-utils";
import EventViewer from "~/components/events/EventViewer.vue";
import { type DefineComponent, defineComponent } from "vue";
import { fakeEventResponse } from "~/test/components/events/constants";
import { getEvents } from "~/composables/useAPIFetch";
import type { EventLogResponse } from "~/services/Api";

vi.mock("~/composables/useAPIFetch", () => ({
getEvents: vi.fn(),
}));

describe("EventViewer.vue", () => {
let EventViewerComponent: DefineComponent<typeof EventViewer>;

beforeEach(() => {
vi.restoreAllMocks();
});

// Render the component with the fake params
beforeAll(async () => {
EventViewerComponent = defineComponent({
components: { EventViewer },
template: "<Suspense><EventViewer/></Suspense>",
});
});

it("Loads events into table", async () => {
vi.mocked(getEvents).mockResolvedValue({
data: ref(fakeEventResponse),
pending: ref(false),
error: ref(null),
status: ref("success"),
refresh: vi.fn(),
execute: vi.fn(),
clear: vi.fn(),
});

const wrapper = mount(EventViewerComponent);

// Wait for async setup to resolve
await flushPromises();
expect(wrapper.text()).toContain("Event Viewer"); // Title of the page
expect(wrapper.text()).toContain("1 event");

// Find header and check content
const headerRow = wrapper.findAll("thead tr th");
expect(headerRow[0].text()).toBe("DateTime"); // First col
expect(headerRow[1].text()).toBe("Event"); // Second col

// Find row and check content
const rows = wrapper.findAll("tbody tr");
expect(rows.length).toBe(1);

const rowCells = rows[0].findAll("td");
expect(rowCells[0].text()).toContain("2/19/26"); // Datetime, limit to date due to TZ issues in runner
expect(rowCells[1].text()).toBe(
"NODE-SETTINGS-GET-SUCCESSHub AdapterNodeInfoA user fetched the node's configurations settings",
);
});

it("No events in response", async () => {
vi.mocked(getEvents).mockResolvedValue({
data: ref<EventLogResponse>({
data: [],
meta: { count: 0, total: 0, limit: 0, offset: 0 },
}),
pending: ref(false),
error: ref(null),
status: ref("success"),
refresh: vi.fn(),
execute: vi.fn(),
clear: vi.fn(),
});

const wrapper = mount(EventViewerComponent);

// Wait for async setup to resolve
await flushPromises();

expect(wrapper.text()).toContain("No events found");
expect(wrapper.text()).toContain("0 events");
});
});
18 changes: 18 additions & 0 deletions test/components/events/TagFilterSidePanel.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { describe, it, expect } from "vitest";
import { mount } from "@vue/test-utils";
import TagFilterSidePanel from "~/components/events/TagFilterSidePanel.vue";

describe("TagFilterSidePanel.vue", () => {
it("renders categories and emits clearTagFilter when clear button clicked", async () => {
const wrapper = mount(TagFilterSidePanel);

// The menu has the two categories
expect(wrapper.text()).toContain("Services");
expect(wrapper.text()).toContain("Log Level");

const button = wrapper.find("button");
await button.trigger("click");

expect(wrapper.emitted()).toHaveProperty("clearTagFilter");
});
});
29 changes: 29 additions & 0 deletions test/components/events/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { EventLogResponse } from "~/services/Api";

export const fakeEventResponse: EventLogResponse = {
data: [
{
id: 683,
event_name: "node.settings.get.success",
service_name: "hub_adapter",
timestamp: "2026-02-19T07:50:14Z",
body: "A user fetched the node's configurations settings",
attributes: {
url: "http://localhost:5000/node/settings",
path: "/node/settings",
tags: ["Hub Adapter", "Node", "Info"],
user: {
email: "foo@bar.baz",
user_id: "f464efc6-0b39-484c-8c24-248fc628dcaf",
username: "someuser",
client_id: "node-ui",
},
client: ["127.0.0.1", 55580],
method: "GET",
service: "node",
status_code: 200,
},
},
],
meta: { count: 1, total: 1, limit: 1, offset: 0 },
};
2 changes: 1 addition & 1 deletion test/components/header/AvatarButton.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe("AvatarButton.vue", () => {

vi.mocked(useRuntimeConfig);

beforeAll(async () => {
beforeAll(() => {
AvatarButtonTestComponent = defineComponent({
components: { AvatarButton },
template: "<Suspense><AvatarButton/></Suspense>",
Expand Down
18 changes: 15 additions & 3 deletions test/components/header/MenuHeader.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import { mount } from "@vue/test-utils";
import { flushPromises, mount } from "@vue/test-utils";
import { useRuntimeConfig } from "#app";
import { describe, expect, it, vi } from "vitest";
import { beforeAll, describe, expect, it, vi } from "vitest";
import MenuHeader from "~/components/header/MenuHeader.vue";
import { type DefineComponent, defineComponent } from "vue";

describe("MenuHeader.vue", () => {
vi.mocked(useRuntimeConfig);

let MenuHeaderTestComponent: DefineComponent<typeof defineComponent>;

// Render the component with the fake params
beforeAll(async () => {
MenuHeaderTestComponent = defineComponent({
components: { MenuHeader },
template: "<Suspense><MenuHeader/></Suspense>",
});
});

async function menuHeaderChecks(authenticated: boolean) {
const status = authenticated ? "authenticated" : "unauthenticated";
const menuTitles = [
Expand All @@ -20,8 +31,9 @@ describe("MenuHeader.vue", () => {
status: ref(status),
}));

const wrapper = mount(MenuHeader);
const wrapper = mount(MenuHeaderTestComponent);
expect(wrapper).toBeTruthy();
await flushPromises();

const menuBar = wrapper.find(".menu-bar-header");

Expand Down
Loading