mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
Read vSphere activity as vCenter's own sentences
The vSphere Activity tab titled every vCenter event with its class name
("HostConnectedEvent", "HostProfileAppliedEvent"), and its When column
clipped the time on every row at most widths.
Pulse never asked vCenter for the readable message: QueryEvents leaves
out fullFormattedMessage unless the filter sets disableFullMessage, so
on a real vCenter the drawer's Message row was always empty too. The
client now sends disableFullMessage: false, and the provider titles an
event change with that message, falling back to the class. That also
gives Patrol's change context a readable reason. The page model prefers
the message for rows stored before this change, and keeps the class as
an Event type row in the drawer and in search.
When now reads as an age ("1d ago"), as Kubernetes events do, with the
exact time on hover and in the drawer. A vCenter time before 2000 (its
unset marker) still shows as "-". The Activity table moves from the
date-time registry rule to the relative-time rule.
This commit is contained in:
@@ -5427,18 +5427,24 @@ Compact platform table timestamps follow the same rule.
|
||||
`PlatformTableDateTimeValue` and `formatPlatformTableDateTimeValue` own compact
|
||||
date-time parsing, invalid/empty markers, optional minimum-year filtering, Intl
|
||||
format options, and tabular-number styling for dense timestamp cells. TrueNAS
|
||||
protection completed-time cells and vSphere activity "When" cells must compose
|
||||
that primitive instead of declaring local compact `toLocaleString` helpers, and
|
||||
timestamp columns use the canonical `numeric-value` alignment kind because they
|
||||
are scannable scalar values.
|
||||
protection completed-time cells must compose that primitive instead of
|
||||
declaring local compact `toLocaleString` helpers, and timestamp columns use the
|
||||
canonical `numeric-value` alignment kind because they are scannable scalar
|
||||
values. vSphere activity "When" cells read as an age instead: the absolute
|
||||
"Oct 4, 11:22 PM" clipped in its column at most widths, and recent vCenter
|
||||
activity is scanned by how long ago it happened, as Kubernetes events are. They
|
||||
compose `PlatformTableRelativeTimeValue` with the exact time in the cell's
|
||||
hover title and in the row drawer, and a vCenter time before 2000 (its unset
|
||||
marker) stays an empty marker rather than a decades-old age.
|
||||
Relative timestamp-age cells follow the same rule.
|
||||
`PlatformTableRelativeTimeValue` and `formatPlatformTableRelativeTimeValue` own
|
||||
the repeated `formatRelativeTime` composition, compact-label default,
|
||||
invalid/empty markers, and tabular-number styling for dense platform table
|
||||
cells. Docker / Podman volume created-at cells, Kubernetes deployment age
|
||||
cells, Kubernetes event observed-time cells, Proxmox backup created-age cells,
|
||||
Proxmox replication last-sync cells, Standalone machine last-seen cells, and
|
||||
Standalone availability-check checked-at cells must compose that primitive
|
||||
Proxmox replication last-sync cells, Standalone machine last-seen cells,
|
||||
Standalone availability-check checked-at cells, and vSphere activity "When"
|
||||
cells must compose that primitive
|
||||
instead of importing `formatRelativeTime` directly or declaring local
|
||||
timestamp-age helpers in table files.
|
||||
Duration and interval cells follow the same rule.
|
||||
|
||||
@@ -1577,6 +1577,15 @@ legacy/direct object routes fall back to `Overview` when the requested workflow
|
||||
has no rows for the current setup. Signals outside unified-resource inventory,
|
||||
such as TrueNAS recovery protection points or vSphere activity timeline rows,
|
||||
must be treated as explicit tab evidence rather than permanent navigation.
|
||||
A vSphere event activity row reads as vCenter's own sentence. The VMware
|
||||
client asks `QueryEvents` for each event's `fullFormattedMessage` explicitly,
|
||||
since vCenter leaves it out when `disableFullMessage` is unset. The provider
|
||||
titles the event change with that message and falls back to the event class
|
||||
(`HostConnectedEvent`) only when vCenter sends none. The class stays in
|
||||
`vmwareEventType` metadata, where activity search and the row drawer's Event
|
||||
type row read it. Rows stored before that titling still carry the class as
|
||||
their title, so the page model prefers an event's `vmwareEventMessage` when it
|
||||
has one.
|
||||
Primary platform navigation is also resource-evidence gated: runtime lenses such
|
||||
as Docker / Podman must be admitted from explicit Docker source scopes, Docker
|
||||
host or service resource types, or concrete runtime identity/inventory evidence
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "af4e6a493cbca1e6c7d4397964efb146e950fca2",
|
||||
"verified_at": "2026-10-06T10:34:56Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/features/vmware/VsphereActivityTable.tsx",
|
||||
"frontend-modern/src/features/vmware/vmwarePageModel.ts"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/features/vmware/VsphereActivityTable.tsx": "389e3884c3a14f608d024661cd02b5f5a10e8bb2f0c331fe3ba519f09c769078",
|
||||
"frontend-modern/src/features/vmware/vmwarePageModel.ts": "df4ae1bc1d02ec68471f78500942e1d0176ebfebf59038ffeb0b2f3c89cc42b2"
|
||||
},
|
||||
"routes": [
|
||||
"/vmware/activity",
|
||||
"/vmware/health"
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
"width": 390,
|
||||
"height": 844
|
||||
},
|
||||
{
|
||||
"width": 600,
|
||||
"height": 844
|
||||
},
|
||||
{
|
||||
"width": 768,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 1024,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 1280,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 1440,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 1920,
|
||||
"height": 1000
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"Private mock stack on main af4e6a493c (which now carries the vSphere Health column, #2559, and the ownership fix, #2560) plus this change, backend rebuilt from the tree; mock vCenter with three tasks and two events whose change records were stored before the change (event class as their title)",
|
||||
"Before (main): event rows read 'HostConnectedEvent' and 'HostProfileAppliedEvent'; the When column clipped 'Oct 4, 11:22 PM' on every row at 390, 768, 1024, 1280 and 1440",
|
||||
"After: event rows read vCenter's own sentence ('Host reconnected to vCenter and is reporting healthy telemetry') including the records stored before the change; tasks keep their task names",
|
||||
"When reads '1d ago' and clips at no width; its hover title is the exact time ('Oct 4, 2026, 11:22 PM'); a vCenter time before 2000 renders '-' (unit test)",
|
||||
"Row drawer: Activity row carries the sentence, a new Event type row carries the class (HostConnectedEvent), and the Message row no longer repeats the title",
|
||||
"Longer event sentences truncate in the Activity column on phones and tablets (390-768) with the full sentence on hover and in the drawer; none clip from 1280",
|
||||
"Light and dark mode at 1440 checked",
|
||||
"After rebasing onto that main: the Activity sweep is identical to the pre-rebase run, and the Datastores and Networks Health columns measure exactly as verified for #2559 (no names clip; only the longest alarm name truncates at 768)"
|
||||
],
|
||||
"interactions": [
|
||||
"Measured every visible header and cell for clipping at seven widths before and after",
|
||||
"Opened an event row's drawer in light and dark mode and read every row",
|
||||
"Read the When cell hover titles",
|
||||
"Searched by event class in the model (unit test): the row it no longer titles is still found"
|
||||
]
|
||||
}
|
||||
@@ -2546,20 +2546,12 @@
|
||||
"export": "PlatformTableDateTimeValue"
|
||||
},
|
||||
"requiredConsumers": [
|
||||
{ "path": "src/features/truenas/TrueNASProtectionTable.tsx" },
|
||||
{ "path": "src/features/vmware/VsphereActivityTable.tsx" }
|
||||
{ "path": "src/features/truenas/TrueNASProtectionTable.tsx" }
|
||||
],
|
||||
"forbiddenPatterns": [
|
||||
{
|
||||
"path": "src/features/truenas/TrueNASProtectionTable.tsx",
|
||||
"patterns": ["const formatPointTime", "formatPointTime(point)"]
|
||||
},
|
||||
{
|
||||
"path": "src/features/vmware/VsphereActivityTable.tsx",
|
||||
"patterns": [
|
||||
"const formatActivityDate",
|
||||
"formatActivityDate(activity.occurredAt || activity.observedAt)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"proof": [
|
||||
@@ -2583,7 +2575,8 @@
|
||||
{ "path": "src/features/proxmox/proxmoxBackupsTableShared.tsx" },
|
||||
{ "path": "src/features/proxmox/ProxmoxReplicationTable.tsx" },
|
||||
{ "path": "src/features/standalone/AvailabilityChecksTable.tsx" },
|
||||
{ "path": "src/features/standalone/AgentsMachinesTable.tsx" }
|
||||
{ "path": "src/features/standalone/AgentsMachinesTable.tsx" },
|
||||
{ "path": "src/features/vmware/VsphereActivityTable.tsx" }
|
||||
],
|
||||
"forbiddenPatterns": [
|
||||
{
|
||||
@@ -2618,6 +2611,13 @@
|
||||
{
|
||||
"path": "src/features/standalone/AvailabilityChecksTable.tsx",
|
||||
"patterns": ["formatRelativeTime(availability?.lastChecked", "const formatChecked"]
|
||||
},
|
||||
{
|
||||
"path": "src/features/vmware/VsphereActivityTable.tsx",
|
||||
"patterns": [
|
||||
"const formatActivityDate",
|
||||
"formatActivityDate(activity.occurredAt || activity.observedAt)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"proof": [
|
||||
@@ -4464,7 +4464,7 @@
|
||||
"summary": "vSphere activity tables must not recreate compact activity-date helpers for timestamp cells.",
|
||||
"canonical": {
|
||||
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
||||
"export": "PlatformTableDateTimeValue"
|
||||
"export": "PlatformTableRelativeTimeValue"
|
||||
},
|
||||
"scopes": ["src/features/vmware/VsphereActivityTable.tsx"],
|
||||
"extensions": [".tsx"],
|
||||
@@ -4473,7 +4473,7 @@
|
||||
"toLocaleString(undefined, {",
|
||||
"minute: '2-digit'"
|
||||
],
|
||||
"legacyReason": "Retired migration debt. Compact platform timestamp cells must compose PlatformTableDateTimeValue so date parsing, empty markers, minimum-year handling, and tabular styling stay shared.",
|
||||
"legacyReason": "Retired migration debt. vSphere activity reads as an age through PlatformTableRelativeTimeValue, with the exact time on hover and in the drawer, so date parsing, empty markers, and tabular styling stay shared.",
|
||||
"allowedPaths": [],
|
||||
"ignoredPaths": [],
|
||||
"proof": [
|
||||
|
||||
@@ -6283,7 +6283,6 @@ describe('shared primitive guardrails', () => {
|
||||
);
|
||||
const platformDateTimeConsumers: Array<[string, string]> = [
|
||||
['src/features/truenas/TrueNASProtectionTable.tsx', truenasProtectionTableSource],
|
||||
['src/features/vmware/VsphereActivityTable.tsx', vsphereActivityTableSource],
|
||||
];
|
||||
const platformDateTimeConsumerPaths = platformDateTimeConsumers.map(([path]) => path);
|
||||
|
||||
@@ -6299,13 +6298,6 @@ describe('shared primitive guardrails', () => {
|
||||
path: 'src/features/truenas/TrueNASProtectionTable.tsx',
|
||||
patterns: ['const formatPointTime', 'formatPointTime(point)'],
|
||||
},
|
||||
{
|
||||
path: 'src/features/vmware/VsphereActivityTable.tsx',
|
||||
patterns: [
|
||||
'const formatActivityDate',
|
||||
'formatActivityDate(activity.occurredAt || activity.observedAt)',
|
||||
],
|
||||
},
|
||||
]);
|
||||
expect(truenasDateGuard?.canonical?.path).toBe(
|
||||
'src/features/platformPage/sharedPlatformPage.tsx',
|
||||
@@ -6322,7 +6314,7 @@ describe('shared primitive guardrails', () => {
|
||||
expect(vsphereDateGuard?.canonical?.path).toBe(
|
||||
'src/features/platformPage/sharedPlatformPage.tsx',
|
||||
);
|
||||
expect(vsphereDateGuard?.canonical?.export).toBe('PlatformTableDateTimeValue');
|
||||
expect(vsphereDateGuard?.canonical?.export).toBe('PlatformTableRelativeTimeValue');
|
||||
expect(vsphereDateGuard?.allPatterns).toEqual([
|
||||
'const formatActivityDate',
|
||||
'toLocaleString(undefined, {',
|
||||
@@ -6346,9 +6338,10 @@ describe('shared primitive guardrails', () => {
|
||||
}
|
||||
|
||||
// The When header renders through the shared sortable head; the
|
||||
// numeric-value alignment intent now lives on its kind prop.
|
||||
// numeric-value alignment intent now lives on its kind prop. vCenter's
|
||||
// unset times (before 2000) stay an empty marker, not a decades-old age.
|
||||
expect(vsphereActivityTableSource).toContain('kind="numeric-value"');
|
||||
expect(vsphereActivityTableSource).toContain('minYear={2000}');
|
||||
expect(vsphereActivityTableSource).toContain('getUTCFullYear() < 2000');
|
||||
});
|
||||
|
||||
it('keeps platform table relative timestamps on the shared primitive', () => {
|
||||
@@ -6384,6 +6377,7 @@ describe('shared primitive guardrails', () => {
|
||||
['src/features/proxmox/ProxmoxReplicationTable.tsx', proxmoxReplicationTableSource],
|
||||
['src/features/standalone/AvailabilityChecksTable.tsx', availabilityChecksTableSource],
|
||||
['src/features/standalone/AgentsMachinesTable.tsx', agentsMachinesTableSource],
|
||||
['src/features/vmware/VsphereActivityTable.tsx', vsphereActivityTableSource],
|
||||
];
|
||||
const platformRelativeTimeConsumerPaths = platformRelativeTimeConsumers.map(([path]) => path);
|
||||
|
||||
@@ -6428,6 +6422,13 @@ describe('shared primitive guardrails', () => {
|
||||
path: 'src/features/standalone/AvailabilityChecksTable.tsx',
|
||||
patterns: ['formatRelativeTime(availability?.lastChecked', 'const formatChecked'],
|
||||
},
|
||||
{
|
||||
path: 'src/features/vmware/VsphereActivityTable.tsx',
|
||||
patterns: [
|
||||
'const formatActivityDate',
|
||||
'formatActivityDate(activity.occurredAt || activity.observedAt)',
|
||||
],
|
||||
},
|
||||
]);
|
||||
expect(localHelperGuard?.canonical?.path).toBe(
|
||||
'src/features/platformPage/sharedPlatformPage.tsx',
|
||||
|
||||
@@ -14,7 +14,7 @@ import { filterChipStatusDot } from '@/components/shared/FilterBar';
|
||||
import {
|
||||
PlatformWindowedRows,
|
||||
PlatformSortableTableHead,
|
||||
PlatformTableDateTimeValue,
|
||||
PlatformTableRelativeTimeValue,
|
||||
PlatformTableEmptyState,
|
||||
PlatformTableToolbar,
|
||||
createPlatformTableFilterState,
|
||||
@@ -145,6 +145,20 @@ const activityTone = (bucket: VmwareActivityStateBucket): ActivityDetailTone =>
|
||||
return 'muted';
|
||||
};
|
||||
|
||||
// An event's title is vCenter's own message when it sent one, so the message
|
||||
// and description rows would only repeat it.
|
||||
const distinctFromTitle = (activity: VmwareActivityRow, value: string): string =>
|
||||
value.trim() && value.trim() !== activity.title.trim() ? value : '';
|
||||
|
||||
// Recent activity reads as an age ("3h ago"); the exact time is on hover and
|
||||
// in the drawer. vCenter reports unset times as dates before 2000.
|
||||
const activityTimestamp = (activity: VmwareActivityRow): string | undefined => {
|
||||
const value = activity.occurredAt || activity.observedAt;
|
||||
if (!value) return undefined;
|
||||
const parsed = new Date(value);
|
||||
return Number.isNaN(parsed.getTime()) || parsed.getUTCFullYear() < 2000 ? undefined : value;
|
||||
};
|
||||
|
||||
const buildActivityDetailSections = (activity: VmwareActivityRow): ActivityDetailSection[] => {
|
||||
const meta = activity.resource.vmware;
|
||||
return compactDetailSections([
|
||||
@@ -156,8 +170,9 @@ const buildActivityDetailSections = (activity: VmwareActivityRow): ActivityDetai
|
||||
tone: activityTone(activity.stateBucket),
|
||||
}),
|
||||
detailRow('Activity', activity.title),
|
||||
detailRow('Message', activity.message),
|
||||
detailRow('Description', activity.description),
|
||||
detailRow('Event type', activity.eventType),
|
||||
detailRow('Message', distinctFromTitle(activity, activity.message)),
|
||||
detailRow('Description', distinctFromTitle(activity, activity.description)),
|
||||
]),
|
||||
},
|
||||
{
|
||||
@@ -421,7 +436,9 @@ export const VsphereActivityTable: Component<{
|
||||
class="block truncate text-base-content"
|
||||
title={[
|
||||
activity.title,
|
||||
activity.message || activity.description || activity.nativeId,
|
||||
distinctFromTitle(activity, activity.message) ||
|
||||
distinctFromTitle(activity, activity.description) ||
|
||||
activity.nativeId,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
@@ -461,11 +478,12 @@ export const VsphereActivityTable: Component<{
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content`}
|
||||
>
|
||||
<PlatformTableDateTimeValue
|
||||
value={activity.occurredAt || activity.observedAt}
|
||||
emptyText="-"
|
||||
minYear={2000}
|
||||
/>
|
||||
<span title={detailDateTime(activityTimestamp(activity))}>
|
||||
<PlatformTableRelativeTimeValue
|
||||
value={activityTimestamp(activity)}
|
||||
emptyText="-"
|
||||
/>
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<Show when={isExpanded()}>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { cleanup, fireEvent, render, screen, within } from '@solidjs/testing-lib
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { VsphereActivityTable } from '@/features/vmware/VsphereActivityTable';
|
||||
import { buildVmwareActivityRows } from '@/features/vmware/vmwarePageModel';
|
||||
import { buildVmwareActivityRows, filterVmwareActivity } from '@/features/vmware/vmwarePageModel';
|
||||
import type { Resource } from '@/types/resource';
|
||||
|
||||
const makeVm = (overrides: Partial<Resource> = {}): Resource =>
|
||||
@@ -55,11 +55,13 @@ const makeVm = (overrides: Partial<Resource> = {}): Resource =>
|
||||
sourceAdapter: 'vmware_adapter',
|
||||
confidence: 'high',
|
||||
actor: 'administrator@vsphere.local',
|
||||
reason: 'VmPoweredOnEvent',
|
||||
reason: 'Virtual machine warehouse-api-01 was powered on',
|
||||
metadata: {
|
||||
activity_type: 'vmware_event',
|
||||
activity_native_id: 'event-501',
|
||||
activity_title: 'VmPoweredOnEvent',
|
||||
// The provider titles an event with vCenter's message, keeping the
|
||||
// event class in vmwareEventType.
|
||||
activity_title: 'Virtual machine warehouse-api-01 was powered on',
|
||||
activity_message: 'Virtual machine warehouse-api-01 was powered on',
|
||||
vmwareEventType: 'VmPoweredOnEvent',
|
||||
vmwareEventMessage: 'Virtual machine warehouse-api-01 was powered on',
|
||||
@@ -103,7 +105,8 @@ describe('VsphereActivityTable', () => {
|
||||
screen.getByTitle('Reconfigure virtual machine · Permission denied while reconfiguring VM'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('Error')).toBeInTheDocument();
|
||||
expect(screen.getByText('VmPoweredOnEvent')).toBeInTheDocument();
|
||||
expect(screen.getByText('Virtual machine warehouse-api-01 was powered on')).toBeInTheDocument();
|
||||
expect(screen.queryByText('VmPoweredOnEvent')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('administrator@vsphere.local')).toBeInTheDocument();
|
||||
|
||||
const row = screen.getByText('Reconfigure virtual machine').closest('tr');
|
||||
@@ -135,4 +138,61 @@ describe('VsphereActivityTable', () => {
|
||||
'false',
|
||||
);
|
||||
});
|
||||
it("reads an event as vCenter's message and its time as an age", async () => {
|
||||
const vm = makeVm();
|
||||
const unset = {
|
||||
...vm.recentChanges![1],
|
||||
id: 'activity-event-unset-time',
|
||||
observedAt: '1970-01-01T00:00:00Z',
|
||||
occurredAt: '1970-01-01T00:00:00Z',
|
||||
reason: 'Alarm status changed on warehouse-api-01',
|
||||
metadata: {
|
||||
...vm.recentChanges![1].metadata,
|
||||
activity_native_id: 'event-502',
|
||||
// Stored before the provider titled events with vCenter's message.
|
||||
activity_title: 'AlarmStatusChangedEvent',
|
||||
activity_message: 'Alarm status changed on warehouse-api-01',
|
||||
vmwareEventType: 'AlarmStatusChangedEvent',
|
||||
vmwareEventMessage: 'Alarm status changed on warehouse-api-01',
|
||||
},
|
||||
};
|
||||
const activity = buildVmwareActivityRows([
|
||||
makeVm({ recentChanges: [...vm.recentChanges!, unset] }),
|
||||
]);
|
||||
|
||||
render(() => (
|
||||
<VsphereActivityTable
|
||||
activity={activity}
|
||||
emptyIcon={<span />}
|
||||
emptyTitle="No activity"
|
||||
emptyDescription="No activity"
|
||||
showToolbar={false}
|
||||
/>
|
||||
));
|
||||
|
||||
// Searching by the event class still finds the row it no longer titles.
|
||||
expect(
|
||||
filterVmwareActivity(activity, 'VmPoweredOnEvent', 'all').map((row) => row.nativeId),
|
||||
).toEqual(['event-501']);
|
||||
|
||||
const powered = screen.getByText('Virtual machine warehouse-api-01 was powered on');
|
||||
// The hover title does not repeat the message the row already shows.
|
||||
expect(powered).toHaveAttribute(
|
||||
'title',
|
||||
'Virtual machine warehouse-api-01 was powered on · event-501',
|
||||
);
|
||||
const poweredRow = powered.closest('tr')!;
|
||||
const when = poweredRow.querySelector('td:last-child span[title]');
|
||||
expect(when?.textContent).toMatch(/ago$/);
|
||||
expect(when?.getAttribute('title')).toMatch(/2026/);
|
||||
|
||||
const unsetRow = screen.getByText('Alarm status changed on warehouse-api-01').closest('tr')!;
|
||||
expect(unsetRow.querySelector('td:last-child')?.textContent).toBe('-');
|
||||
|
||||
await fireEvent.click(poweredRow);
|
||||
const detail = within(screen.getByTestId('vsphere-activity-detail'));
|
||||
expect(detail.getByText('Event type')).toBeInTheDocument();
|
||||
expect(detail.getByText('VmPoweredOnEvent')).toBeInTheDocument();
|
||||
expect(detail.queryByText('Message')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -522,10 +522,13 @@ describe('vmwarePageModel', () => {
|
||||
|
||||
expect(mapVmwareActivityStateBucket('error')).toBe('failed');
|
||||
expect(mapVmwareActivityStateBucket('success')).toBe('success');
|
||||
// An event reads as vCenter's message even when its stored title is the
|
||||
// event class; the class stays on the row for search and the drawer.
|
||||
expect(rows.map((row) => row.title)).toEqual([
|
||||
'Reconfigure virtual machine',
|
||||
'VmPoweredOnEvent',
|
||||
'Virtual machine warehouse-api-01 was powered on',
|
||||
]);
|
||||
expect(rows[1]?.eventType).toBe('VmPoweredOnEvent');
|
||||
expect(rows[0]).toMatchObject({
|
||||
resourceName: 'warehouse-api-01',
|
||||
activityKind: 'task',
|
||||
|
||||
@@ -86,6 +86,9 @@ export type VmwareActivityRow = {
|
||||
activityType: string;
|
||||
stateBucket: VmwareActivityStateBucket;
|
||||
title: string;
|
||||
// vCenter's event class (HostConnectedEvent); the title reads as vCenter's
|
||||
// own message when it sent one, so the class is kept for search and detail.
|
||||
eventType: string;
|
||||
state: string;
|
||||
message: string;
|
||||
description: string;
|
||||
@@ -795,7 +798,10 @@ const buildActivityRow = (
|
||||
const metadata = change.metadata;
|
||||
const activityType = metadataString(metadata, 'activity_type');
|
||||
const activityKind = mapVmwareActivityKind(activityType, metadata);
|
||||
// An event reads as vCenter's own message. The provider titles new events
|
||||
// with it; records stored before that carry the event class as their title.
|
||||
const title =
|
||||
(activityKind === 'event' ? metadataString(metadata, 'vmwareEventMessage') : '') ||
|
||||
metadataString(metadata, 'activity_title', 'vmwareTaskName', 'vmwareEventType') ||
|
||||
trimString(change.reason) ||
|
||||
'vSphere activity';
|
||||
@@ -805,6 +811,7 @@ const buildActivityRow = (
|
||||
metadataString(metadata, 'activity_message', 'vmwareTaskError', 'vmwareEventMessage') ||
|
||||
trimString(change.reason);
|
||||
const description = metadataString(metadata, 'vmwareTaskDescription', 'vmwareEventMessage');
|
||||
const eventType = metadataString(metadata, 'vmwareEventType');
|
||||
const actor = trimString(change.actor) || metadataString(metadata, 'vmwareEventUser');
|
||||
const nativeId = metadataString(metadata, 'activity_native_id', 'vmwareTask', 'vmwareEvent');
|
||||
const observedAt = trimString(change.observedAt);
|
||||
@@ -829,6 +836,7 @@ const buildActivityRow = (
|
||||
activityType,
|
||||
stateBucket: mapVmwareActivityStateBucket(state),
|
||||
title,
|
||||
eventType,
|
||||
state,
|
||||
message,
|
||||
description,
|
||||
@@ -1112,6 +1120,7 @@ const activitySearchHaystack = (row: VmwareActivityRow): string =>
|
||||
row.activityKind,
|
||||
row.activityType,
|
||||
row.title,
|
||||
row.eventType,
|
||||
row.state,
|
||||
row.message,
|
||||
row.description,
|
||||
|
||||
@@ -88,10 +88,13 @@ func entityActivityChanges(resourceID, connectionID, entityType, managedObjectID
|
||||
SourceAdapter: unifiedresources.AdapterVMware,
|
||||
ActivityType: "vmware_event",
|
||||
NativeID: strings.TrimSpace(event.Event),
|
||||
Title: firstNonEmptyTrimmed(event.Type),
|
||||
Message: strings.TrimSpace(event.Message),
|
||||
Actor: strings.TrimSpace(event.User),
|
||||
OccurredAt: event.CreatedAt,
|
||||
// vCenter's own sentence ("Host esxi-01 in Primary DC is connected")
|
||||
// reads where the event class (HostConnectedEvent) does not; the
|
||||
// class stays in vmwareEventType for search and the drawer.
|
||||
Title: firstNonEmptyTrimmed(event.Message, event.Type),
|
||||
Message: strings.TrimSpace(event.Message),
|
||||
Actor: strings.TrimSpace(event.User),
|
||||
OccurredAt: event.CreatedAt,
|
||||
Metadata: map[string]any{
|
||||
"vmwareConnectionId": strings.TrimSpace(connectionID),
|
||||
"vmwareEntityType": strings.TrimSpace(entityType),
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package vmware
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
||||
)
|
||||
|
||||
// An event reads as vCenter's own sentence; the event class is the fallback
|
||||
// when vCenter sends no message, and it stays in metadata for search.
|
||||
func TestVMwareEventActivityReadsAsVCenterMessage(t *testing.T) {
|
||||
changes := entityActivityChanges("agent-host-101", "vc-1", "host", "host-101", nil, []InventoryEvent{
|
||||
{Event: "event-1", Type: "HostConnectedEvent", Message: "Host esxi-01 in Primary DC is connected"},
|
||||
{Event: "event-2", Type: "HostProfileAppliedEvent"},
|
||||
})
|
||||
if len(changes) != 2 {
|
||||
t.Fatalf("changes = %+v, want two event changes", changes)
|
||||
}
|
||||
|
||||
byEvent := map[string]unifiedresources.ResourceChange{}
|
||||
for _, change := range changes {
|
||||
byEvent[change.Metadata["vmwareEvent"].(string)] = change
|
||||
}
|
||||
|
||||
messaged := byEvent["event-1"]
|
||||
if messaged.Reason != "Host esxi-01 in Primary DC is connected" {
|
||||
t.Fatalf("messaged event reason = %q, want vCenter's message", messaged.Reason)
|
||||
}
|
||||
if got := messaged.Metadata[unifiedresources.MetadataActivityTitle]; got != "Host esxi-01 in Primary DC is connected" {
|
||||
t.Fatalf("messaged event title = %v, want vCenter's message", got)
|
||||
}
|
||||
if got := messaged.Metadata["vmwareEventType"]; got != "HostConnectedEvent" {
|
||||
t.Fatalf("event class = %v, want it kept in metadata", got)
|
||||
}
|
||||
|
||||
if got := byEvent["event-2"].Reason; got != "HostProfileAppliedEvent" {
|
||||
t.Fatalf("unmessaged event reason = %q, want the event class", got)
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,9 @@ type viJSONEventFilterSpec struct {
|
||||
TypeName string `json:"_typeName,omitempty"`
|
||||
Entity *viJSONEventFilterEntity `json:"entity,omitempty"`
|
||||
MaxCount int `json:"maxCount,omitempty"`
|
||||
// vCenter omits each event's fullFormattedMessage unless the filter sets
|
||||
// this explicitly, so it is always sent: false asks for the message.
|
||||
DisableFullMessage bool `json:"disableFullMessage"`
|
||||
}
|
||||
|
||||
type viJSONEventFilterEntity struct {
|
||||
|
||||
@@ -96,6 +96,8 @@ func TestQueryEventsRequestCarriesTypeNameDiscriminators(t *testing.T) {
|
||||
`"_typeName":"EventFilterSpec"`,
|
||||
`"_typeName":"EventFilterSpecByEntity"`,
|
||||
`"_typeName":"ManagedObjectReference"`,
|
||||
// Unset, vCenter leaves out every event's readable message.
|
||||
`"disableFullMessage":false`,
|
||||
} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Fatalf("QueryEvents request %s missing %s", body, want)
|
||||
|
||||
Reference in New Issue
Block a user