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:
rcourtman
2026-10-06 11:34:56 +01:00
parent af4e6a493c
commit b5444cf8bf
13 changed files with 264 additions and 47 deletions
@@ -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,
+7 -4
View File
@@ -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)
}
}
+3
View File
@@ -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)