mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Fix vSphere workload first paint
This commit is contained in:
@@ -71,14 +71,15 @@ shared filter rail must remove that route value along with the other active
|
||||
facets; the feature must not replace the shared rail with a page-local select.
|
||||
|
||||
Large-estate platform pages must keep one canonical inventory snapshot for the
|
||||
initial read and explicit refresh path. The Proxmox overview owns its unified
|
||||
resource request and passes that snapshot into the shared workloads state; the
|
||||
workloads adapter may map the snapshot into the legacy guest boundary, but must
|
||||
not issue a second workload inventory request or create a second infrastructure
|
||||
poll. Refreshing the overview must invalidate that owner snapshot and update
|
||||
both the node and guest regions from the same result, so a large estate cannot
|
||||
render contradictory counts or pay duplicate transport and reconciliation
|
||||
costs.
|
||||
initial read and explicit refresh path. The Proxmox and VMware vSphere
|
||||
overviews own their source-scoped unified-resource requests and pass those
|
||||
snapshots into the shared workloads state; the workloads adapter may map the
|
||||
snapshot into the legacy guest boundary, but must not issue a second workload
|
||||
inventory request or create a second infrastructure poll. Refreshing an
|
||||
overview must invalidate that owner snapshot and update both the host/node and
|
||||
guest regions from the same result, so a large estate cannot render
|
||||
contradictory counts, flash a false empty workload state, or pay duplicate
|
||||
transport and reconciliation costs.
|
||||
The overview's bounded structural summary remains ahead of its long virtualized
|
||||
inventory at desktop and narrow widths. In particular, Proxmox must show up to
|
||||
six phone rows or twelve larger-layout rows before the guest list instead of
|
||||
|
||||
@@ -1360,6 +1360,19 @@ an interim unknown or never-backed-up state while waiting for a WebSocket
|
||||
tick. This correction adds no request, subscription, retry, or resource scan;
|
||||
`useUnifiedResources.test.ts` and `useWorkloads.test.ts` pin the boundary.
|
||||
|
||||
### vSphere owner snapshots avoid duplicate estate enumeration
|
||||
|
||||
The VMware vSphere overview owns one `type=agent,vm,storage,network` resource
|
||||
query scoped to `source=vmware-vsphere` and passes the resulting canonical
|
||||
snapshot into its embedded Workloads state. Host and VM regions therefore
|
||||
paint from one paged generation and explicit refresh path; the Workloads owner
|
||||
must not launch the generic all-platform workload pagination or its generic
|
||||
infrastructure snapshot. The shared unified-resource hook projects the same
|
||||
type-and-source filter from canonical WebSocket state, so this request
|
||||
reduction adds no polling fallback or freshness regression. Contract tests pin
|
||||
the request shape, realtime projection, snapshot injection, and refresh
|
||||
handoff.
|
||||
|
||||
### Command-session liveness lookup stays bounded and in-memory
|
||||
|
||||
The connections ledger's command-channel liveness check
|
||||
|
||||
@@ -25,6 +25,15 @@ The shared `internal/api/` helper-binary download route is isolated from
|
||||
storage and recovery semantics. `/download/pulse-agent-helper` serves a signed
|
||||
Linux runtime artifact only; it cannot read, mutate, restore, export, or select
|
||||
storage state, recovery timelines, backup providers, or repository scope.
|
||||
The parallel `/download/pulse-agent-runner` artifact route and
|
||||
`/api/agents/action-runner/credential` issuance route are also adjacent
|
||||
API/agent-lifecycle boundaries. A signed runner binary or host-bound typed
|
||||
action credential is not a backup, snapshot, recovery point, restore grant,
|
||||
retention decision, or repository selector. Safe-profile inspect/apply/rollback
|
||||
preserves collector identity and installation files only; it must not rewrite
|
||||
storage/recovery evidence or reinterpret a restored legacy collector profile as
|
||||
recovery success. Typed `host.storage_cleanup` remains governed remediation,
|
||||
not storage-recovery authority inferred from the credential itself.
|
||||
|
||||
First-run authentication always writes the canonical `.env` persistence
|
||||
artifact before runtime state changes. Root systemd installation may also
|
||||
@@ -53,6 +62,14 @@ workload or infrastructure inventory request. Backup and recovery tables keep
|
||||
their domain-specific evidence requests, while an overview refresh must update
|
||||
all overview regions from the same owner snapshot rather than mixing reads
|
||||
from different inventory generations.
|
||||
The vSphere overview follows the same read-side ownership rule for its host,
|
||||
VM, datastore, and network regions. Its canonical snapshot is constrained to
|
||||
the `vmware-vsphere` source at the resource API boundary, and the embedded
|
||||
workloads adapter consumes that page-owned snapshot instead of issuing a
|
||||
second generic inventory request. Source aliases used to project a canonical
|
||||
websocket snapshot are transport filtering only: a VMware datastore remains
|
||||
descriptive inventory and does not gain backup, protection, recovery-point,
|
||||
or restore semantics from that projection.
|
||||
The physical-disks surface consumes every page in the server-declared unified
|
||||
resource result, scopes same-named Proxmox nodes by instance, and never hides a
|
||||
disk behind a client-side page ceiling. Search includes vendor, WWN, transport,
|
||||
|
||||
@@ -2046,6 +2046,14 @@ without waiting for a later WebSocket merge; REST and WebSocket delivery must
|
||||
not disagree about already-authoritative fields. The boundary is pinned by
|
||||
`useUnifiedResources.test.ts` and `useWorkloads.test.ts`.
|
||||
|
||||
Source-scoped platform owners remain canonical realtime consumers. The shared
|
||||
frontend hook locally projects supported `type` plus `source` filters from the
|
||||
canonical WebSocket snapshot, including the public `vmware-vsphere` alias for
|
||||
the raw `vmware` source, rather than degrading a source-scoped page to periodic
|
||||
REST refreshes. VMware Overview passes that same source-scoped snapshot into
|
||||
the embedded Workloads state, so hosts and VMs share one inventory generation
|
||||
and one explicit refresh path.
|
||||
|
||||
### Agent libvirt domains use a provider-neutral VM facet
|
||||
|
||||
The registry projects a host's validated libvirt domains as `vm` resources
|
||||
|
||||
@@ -1,31 +1,23 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "6453593b4b984eaa7d1d40727f43e12528212dd7",
|
||||
"verified_at": "2026-08-29T21:11:53Z",
|
||||
"base_sha": "6d4ee48000f9266d161af000f88d9d1f10042328",
|
||||
"verified_at": "2026-08-29T22:35:09Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/api/agentDiagnostics.ts",
|
||||
"frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx",
|
||||
"frontend-modern/src/components/Settings/NodeModalSetupGuideSection.tsx",
|
||||
"frontend-modern/src/components/Settings/infrastructureAgentUpdateCommandsModel.ts",
|
||||
"frontend-modern/src/components/Settings/useNodeModalState.ts"
|
||||
"frontend-modern/src/features/vmware/VmwarePageSurface.tsx",
|
||||
"frontend-modern/src/hooks/useUnifiedResources.ts"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/api/agentDiagnostics.ts": "0bf0f3d0854ebd03ab9d80ccef0e39655aeeb2bd16d18901b527ba3955a086bb",
|
||||
"frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx": "0fe6f4cb3f10adbf13306eaa73b390e24b4d586ec71479cb7c8494ce158e6302",
|
||||
"frontend-modern/src/components/Settings/NodeModalSetupGuideSection.tsx": "5aa559093039bc18785b4df88c2c955d67465b969b85979bc2a836a7d64d6dd0",
|
||||
"frontend-modern/src/components/Settings/infrastructureAgentUpdateCommandsModel.ts": "d352500b9f804111fe07602aa3716e9d1ff1a30ac5fe3b1d6a7d0587c36c6171",
|
||||
"frontend-modern/src/components/Settings/useNodeModalState.ts": "0f198074f88ef8d55a0fdc5142bacee4a39b572b12f94f7ed88a0fa10724c067"
|
||||
"frontend-modern/src/features/vmware/VmwarePageSurface.tsx": "659851ff4111f53d15f74d7959ab9e3f21683eecf0e5c55d91e477f47ad93540",
|
||||
"frontend-modern/src/hooks/useUnifiedResources.ts": "0b5cca6992ae17575e5f689268e152cbdd47f61b3169043cec314b27c11d8c97"
|
||||
},
|
||||
"routes": [
|
||||
"/settings/infrastructure?add=docker",
|
||||
"/settings/infrastructure?add=pve",
|
||||
"/settings/infrastructure/agent-doctor"
|
||||
"/vmware/overview"
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
"width": 1440,
|
||||
"height": 1000
|
||||
"width": 1280,
|
||||
"height": 720
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
@@ -33,15 +25,14 @@
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"mock-backed Docker installer before token generation, including the transitional Docker-in-LXC trust notice",
|
||||
"Proxmox VE onboarding with the API-first strategy selected",
|
||||
"Proxmox VE onboarding with Host Telemetry Agent selected and monitoring-only credential guidance visible",
|
||||
"Agent Doctor route with the current fleet diagnostics loaded"
|
||||
"vSphere overview with seven ESXi hosts and eighteen VMs loaded from one source-scoped canonical inventory snapshot",
|
||||
"desktop host table, workload filters, grouped VM table, and live-connection status",
|
||||
"narrow host and VM table projections with the responsive filter control and fixed mobile navigation"
|
||||
],
|
||||
"interactions": [
|
||||
"opened the Docker onboarding deep link and verified monitoring-first copy, token authorization guidance, and install handoff at desktop width",
|
||||
"narrowed the Docker onboarding dialog to 390 pixels and verified the title, Docker-in-LXC notice, and token action remained visible",
|
||||
"opened Proxmox VE onboarding, switched from Connect via API to Host Telemetry Agent, and verified the monitoring-only command-authority boundary copy",
|
||||
"opened Agent Doctor and verified the routed diagnostics surface loaded without console-blocking UI failures"
|
||||
"reloaded the vSphere overview with browser request tracing enabled and verified the only resource-list request was the page-owned type-and-source-scoped snapshot",
|
||||
"verified the embedded VM filter and table populated from the owner snapshot without a generic workload or infrastructure resource request",
|
||||
"set a 390 by 844 viewport and verified the host rows, VM search, Filters action, grouped VM rows, and mobile navigation remained visible and usable",
|
||||
"reset the temporary viewport override to the browser default after responsive verification"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
} from '@/components/Workloads/workloadMetricHistoryModel';
|
||||
import { usePersistentSignal } from '@/hooks/usePersistentSignal';
|
||||
import { STORAGE_KEYS } from '@/utils/localStorage';
|
||||
import type { Resource } from '@/types/resource';
|
||||
import { VsphereHostsTable } from './VsphereHostsTable';
|
||||
import {
|
||||
VMWARE_TAB_SPECS,
|
||||
@@ -57,7 +58,7 @@ import { VsphereNetworksTable } from './VsphereNetworksTable';
|
||||
// as canonical `vm`, datastores as canonical `storage`, and vCenter networks
|
||||
// as canonical `network`; provider-native topology stays in VMware metadata
|
||||
// under those shared resources.
|
||||
const VMWARE_RESOURCE_QUERY = 'type=agent,vm,storage,network';
|
||||
const VMWARE_RESOURCE_QUERY = 'type=agent,vm,storage,network&source=vmware-vsphere';
|
||||
const VALID_TABS = new Set<VmwarePageTabId>(VMWARE_TAB_SPECS.map((tab) => tab.id));
|
||||
|
||||
const VMWARE_PLATFORM_FILTER = 'vmware-vsphere';
|
||||
@@ -276,6 +277,10 @@ export function VmwarePageSurface() {
|
||||
metricHistoryRange={metricHistoryRange}
|
||||
setMetricHistoryRange={setMetricHistoryRange}
|
||||
inventorySourcesQuery={inventorySources}
|
||||
resourceSnapshot={() =>
|
||||
loading() && model().resources.length === 0 ? undefined : model().resources
|
||||
}
|
||||
resourceSnapshotRefetch={() => refetch()}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
@@ -348,6 +353,8 @@ interface VmwareOverviewProps {
|
||||
metricHistoryRange: Accessor<WorkloadTableMetricHistoryRange>;
|
||||
setMetricHistoryRange: (value: WorkloadTableMetricHistoryRange) => void;
|
||||
inventorySourcesQuery: WorkloadsInventorySourcesQuery;
|
||||
resourceSnapshot: Accessor<Resource[] | undefined>;
|
||||
resourceSnapshotRefetch: () => Promise<unknown>;
|
||||
}
|
||||
|
||||
function VmwareOverview(props: VmwareOverviewProps) {
|
||||
@@ -356,6 +363,8 @@ function VmwareOverview(props: VmwareOverviewProps) {
|
||||
containers: [],
|
||||
nodes: [],
|
||||
useWorkloads: true,
|
||||
resourceSnapshot: props.resourceSnapshot,
|
||||
resourceSnapshotRefetch: props.resourceSnapshotRefetch,
|
||||
forcedPlatform: VMWARE_PLATFORM_FILTER,
|
||||
forcedViewMode: 'vm',
|
||||
suppressPlatformFilter: true,
|
||||
|
||||
@@ -8,6 +8,7 @@ const mockPathname = vi.hoisted(() => vi.fn(() => '/vmware/overview'));
|
||||
const mockVersionInfo = vi.hoisted(() => vi.fn());
|
||||
const mockGetGlobalTimeline = vi.hoisted(() => vi.fn());
|
||||
const mockListInventorySources = vi.hoisted(() => vi.fn());
|
||||
const mockUseWorkloadsState = vi.hoisted(() => vi.fn());
|
||||
|
||||
const makeResource = (resource: Partial<Resource> & Pick<Resource, 'id' | 'type'>): Resource =>
|
||||
({
|
||||
@@ -21,13 +22,18 @@ const makeResource = (resource: Partial<Resource> & Pick<Resource, 'id' | 'type'
|
||||
...resource,
|
||||
}) as Resource;
|
||||
|
||||
const setResources = (resources: Resource[]) => {
|
||||
const setResources = (
|
||||
resources: Resource[],
|
||||
options: { loading?: boolean; refetch?: ReturnType<typeof vi.fn> } = {},
|
||||
) => {
|
||||
const refetch = options.refetch ?? vi.fn().mockResolvedValue(resources);
|
||||
mockUseUnifiedResources.mockReturnValue({
|
||||
resources: () => resources,
|
||||
loading: () => false,
|
||||
loading: () => options.loading ?? false,
|
||||
error: () => null,
|
||||
refetch: vi.fn(),
|
||||
refetch,
|
||||
});
|
||||
return refetch;
|
||||
};
|
||||
|
||||
vi.mock('@/hooks/useUnifiedResources', () => ({
|
||||
@@ -73,13 +79,7 @@ vi.mock('@/components/Workloads/WorkloadsSurface', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('@/components/Workloads/useWorkloadsState', () => ({
|
||||
useWorkloadsState: () => ({
|
||||
surfaceConnected: () => false,
|
||||
surfaceInitialDataReceived: () => false,
|
||||
allGuests: () => [],
|
||||
search: () => '',
|
||||
setSearch: vi.fn(),
|
||||
}),
|
||||
useWorkloadsState: (...args: unknown[]) => mockUseWorkloadsState(...args),
|
||||
}));
|
||||
|
||||
vi.mock('@/features/platformPage/sharedPlatformPage', () => ({
|
||||
@@ -126,6 +126,13 @@ describe('VmwarePageSurface contract', () => {
|
||||
mockVersionInfo.mockReturnValue(null);
|
||||
mockGetGlobalTimeline.mockResolvedValue({ recentChanges: [] });
|
||||
mockListInventorySources.mockResolvedValue({ sources: [] });
|
||||
mockUseWorkloadsState.mockReturnValue({
|
||||
surfaceConnected: () => false,
|
||||
surfaceInitialDataReceived: () => false,
|
||||
allGuests: () => [],
|
||||
search: () => '',
|
||||
setSearch: vi.fn(),
|
||||
});
|
||||
setResources([]);
|
||||
});
|
||||
|
||||
@@ -171,6 +178,49 @@ describe('VmwarePageSurface contract', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('reuses the source-scoped page snapshot for the vSphere workload surface', async () => {
|
||||
const refetch = vi.fn().mockResolvedValue([]);
|
||||
const resources = [
|
||||
makeResource({
|
||||
id: 'esxi-host-1',
|
||||
type: 'agent',
|
||||
vmware: { entityType: 'host', managedObjectId: 'host-1' },
|
||||
}),
|
||||
makeResource({
|
||||
id: 'vm-app-01',
|
||||
type: 'vm',
|
||||
vmware: { entityType: 'vm', managedObjectId: 'vm-1' },
|
||||
}),
|
||||
makeResource({
|
||||
id: 'other-platform-vm',
|
||||
type: 'vm',
|
||||
platformType: 'proxmox-pve',
|
||||
vmware: undefined,
|
||||
}),
|
||||
];
|
||||
setResources(resources, { refetch });
|
||||
|
||||
render(() => <VmwarePageSurface />);
|
||||
|
||||
expect(mockUseUnifiedResources).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
query: 'type=agent,vm,storage,network&source=vmware-vsphere',
|
||||
cacheKey: 'vmware-workspace',
|
||||
}),
|
||||
);
|
||||
const stateOptions = mockUseWorkloadsState.mock.calls[0]?.[0] as {
|
||||
resourceSnapshot: () => Resource[] | undefined;
|
||||
resourceSnapshotRefetch: () => Promise<unknown>;
|
||||
};
|
||||
expect(stateOptions.resourceSnapshot()?.map((resource) => resource.id)).toEqual([
|
||||
'esxi-host-1',
|
||||
'vm-app-01',
|
||||
]);
|
||||
|
||||
await stateOptions.resourceSnapshotRefetch();
|
||||
expect(refetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not treat vSphere ESXi API host resources as Pulse agent update targets', () => {
|
||||
mockVersionInfo.mockReturnValue({
|
||||
version: 'v6.0.0-rc.6',
|
||||
|
||||
@@ -663,6 +663,96 @@ describe('useUnifiedResources', () => {
|
||||
dispose();
|
||||
});
|
||||
|
||||
it('keeps source filters in the canonical resource request', async () => {
|
||||
let dispose = () => {};
|
||||
let result: ReturnType<UseUnifiedResourcesModule['useUnifiedResources']> | undefined;
|
||||
createRoot((d) => {
|
||||
dispose = d;
|
||||
result = useUnifiedResources({
|
||||
query: 'type=agent,vm,storage,network&source=vmware-vsphere',
|
||||
cacheKey: 'vmware-overview-contract',
|
||||
});
|
||||
});
|
||||
|
||||
await flushAsync();
|
||||
expect(apiFetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(apiFetchMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'/api/resources?type=agent%2Cvm%2Cstorage%2Cnetwork&source=vmware-vsphere&page=1&limit=100',
|
||||
{ cache: 'no-store' },
|
||||
);
|
||||
await waitForResourceCount(() => result!.resources().length);
|
||||
|
||||
dispose();
|
||||
});
|
||||
|
||||
it('projects source-scoped queries from the canonical websocket snapshot', async () => {
|
||||
const vmwareVM = createWsResource({
|
||||
id: 'vmware-vm-1',
|
||||
type: 'vm',
|
||||
name: 'vmware-vm-1',
|
||||
displayName: 'vmware-vm-1',
|
||||
platformId: 'vmware-vm-1',
|
||||
platformType: 'vmware-vsphere',
|
||||
sourceType: 'api',
|
||||
sources: ['vmware'],
|
||||
});
|
||||
const proxmoxVM = createWsResource({
|
||||
id: 'proxmox-vm-1',
|
||||
type: 'vm',
|
||||
name: 'proxmox-vm-1',
|
||||
displayName: 'proxmox-vm-1',
|
||||
platformId: 'proxmox-vm-1',
|
||||
platformType: 'proxmox-pve',
|
||||
sourceType: 'api',
|
||||
sources: ['proxmox'],
|
||||
});
|
||||
batch(() => {
|
||||
setWsState('resources', [vmwareVM, proxmoxVM]);
|
||||
setWsState('lastUpdate', 1738843200100);
|
||||
setWsResourceChange({
|
||||
version: 1738843200100,
|
||||
changedIds: new Set([vmwareVM.id, proxmoxVM.id]),
|
||||
});
|
||||
});
|
||||
|
||||
let dispose = () => {};
|
||||
let result: ReturnType<UseUnifiedResourcesModule['useUnifiedResources']> | undefined;
|
||||
createRoot((d) => {
|
||||
dispose = d;
|
||||
result = useUnifiedResources({
|
||||
query: 'type=agent,vm&source=vmware-vsphere',
|
||||
cacheKey: 'vmware-websocket-projection',
|
||||
initialHydration: 'prefer-ws',
|
||||
});
|
||||
});
|
||||
|
||||
await waitForValue(() => result!.resources().length, 1);
|
||||
expect(result!.resources().map((resource) => resource.id)).toEqual(['vmware-vm-1']);
|
||||
expect(apiFetchMock).not.toHaveBeenCalled();
|
||||
|
||||
dispose();
|
||||
});
|
||||
|
||||
it('keeps unsupported source filters on the canonical REST fallback path', async () => {
|
||||
let dispose = () => {};
|
||||
let result: ReturnType<UseUnifiedResourcesModule['useUnifiedResources']> | undefined;
|
||||
createRoot((d) => {
|
||||
dispose = d;
|
||||
result = useUnifiedResources({
|
||||
query: 'source=future-provider',
|
||||
cacheKey: 'unsupported-source-filter',
|
||||
initialHydration: 'prefer-ws',
|
||||
});
|
||||
});
|
||||
|
||||
await flushAsync();
|
||||
expect(apiFetchMock).toHaveBeenCalledTimes(1);
|
||||
await waitForResourceCount(() => result!.resources().length);
|
||||
|
||||
dispose();
|
||||
});
|
||||
|
||||
it('loads physical disks beyond the first resource page without dropping wide-node inventory', async () => {
|
||||
const firstPage = Array.from({ length: 100 }, (_, index) => ({
|
||||
...v2Resource,
|
||||
|
||||
@@ -1130,7 +1130,36 @@ const enrichCanonicalAllResourcesCache = (
|
||||
setUnifiedResourcesCache(entry, enriched);
|
||||
};
|
||||
|
||||
const parseUnifiedResourcesTypeFilter = (query: string): Set<ResourceType> | null => {
|
||||
type UnifiedResourcesLocalFilter = {
|
||||
types: Set<ResourceType>;
|
||||
sources: Set<string>;
|
||||
};
|
||||
|
||||
const normalizeUnifiedResourceSource = (source: string): string => {
|
||||
const normalized = source.trim().toLowerCase();
|
||||
switch (normalized) {
|
||||
case 'k8s':
|
||||
return 'kubernetes';
|
||||
case 'vmware-vsphere':
|
||||
return 'vmware';
|
||||
default:
|
||||
return normalized;
|
||||
}
|
||||
};
|
||||
|
||||
const SUPPORTED_UNIFIED_RESOURCE_SOURCES = new Set([
|
||||
'proxmox',
|
||||
'agent',
|
||||
'docker',
|
||||
'pbs',
|
||||
'pmg',
|
||||
'kubernetes',
|
||||
'truenas',
|
||||
'vmware',
|
||||
'availability',
|
||||
]);
|
||||
|
||||
const parseUnifiedResourcesLocalFilter = (query: string): UnifiedResourcesLocalFilter | null => {
|
||||
const normalizedQuery = normalizeUnifiedResourcesQuery(query);
|
||||
if (normalizedQuery === '') {
|
||||
return null;
|
||||
@@ -1138,9 +1167,10 @@ const parseUnifiedResourcesTypeFilter = (query: string): Set<ResourceType> | nul
|
||||
|
||||
const params = new URLSearchParams(normalizedQuery);
|
||||
const types = new Set<ResourceType>();
|
||||
const sources = new Set<string>();
|
||||
|
||||
for (const [key, value] of params.entries()) {
|
||||
if (key !== 'type') {
|
||||
if (key !== 'type' && key !== 'source') {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1149,26 +1179,50 @@ const parseUnifiedResourcesTypeFilter = (query: string): Set<ResourceType> | nul
|
||||
.map((candidate) => asTrimmedString(candidate))
|
||||
.filter((candidate): candidate is string => candidate !== undefined)
|
||||
.forEach((candidate) => {
|
||||
types.add(resolveType(candidate));
|
||||
if (key === 'type') {
|
||||
types.add(resolveType(candidate));
|
||||
return;
|
||||
}
|
||||
const normalizedSource = normalizeUnifiedResourceSource(candidate);
|
||||
if (SUPPORTED_UNIFIED_RESOURCE_SOURCES.has(normalizedSource)) {
|
||||
sources.add(normalizedSource);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return types.size > 0 ? types : null;
|
||||
return types.size > 0 || sources.size > 0 ? { types, sources } : null;
|
||||
};
|
||||
|
||||
const filterCanonicalUnifiedResources = (
|
||||
resources: Resource[],
|
||||
query: string,
|
||||
typeFilter: Set<ResourceType> | null,
|
||||
localFilter: UnifiedResourcesLocalFilter | null,
|
||||
): Resource[] | null => {
|
||||
const normalizedQuery = normalizeUnifiedResourcesQuery(query);
|
||||
if (normalizedQuery === '') {
|
||||
return resources;
|
||||
}
|
||||
if (!typeFilter) {
|
||||
if (!localFilter) {
|
||||
return null;
|
||||
}
|
||||
return resources.filter((resource) => typeFilter.has(resolveType(resource.type)));
|
||||
return resources.filter((resource) => {
|
||||
if (localFilter.types.size > 0 && !localFilter.types.has(resolveType(resource.type))) {
|
||||
return false;
|
||||
}
|
||||
if (localFilter.sources.size === 0) {
|
||||
return true;
|
||||
}
|
||||
const platformSources = resource.platformData?.sources;
|
||||
const resourceSources = [
|
||||
...(resource.sources ?? []),
|
||||
...(Array.isArray(platformSources)
|
||||
? platformSources.filter((source): source is string => typeof source === 'string')
|
||||
: []),
|
||||
];
|
||||
return resourceSources.some((source) =>
|
||||
localFilter.sources.has(normalizeUnifiedResourceSource(source)),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1213,8 +1267,8 @@ const seedUnifiedResourcesCacheFromAllResources = (
|
||||
return entry;
|
||||
}
|
||||
|
||||
const typeFilter = parseUnifiedResourcesTypeFilter(query);
|
||||
if (!typeFilter) {
|
||||
const localFilter = parseUnifiedResourcesLocalFilter(query);
|
||||
if (!localFilter) {
|
||||
return entry;
|
||||
}
|
||||
|
||||
@@ -1225,9 +1279,8 @@ const seedUnifiedResourcesCacheFromAllResources = (
|
||||
return entry;
|
||||
}
|
||||
|
||||
entry.resources = allResourcesEntry.resources.filter((resource) =>
|
||||
typeFilter.has(resolveType(resource.type)),
|
||||
);
|
||||
entry.resources =
|
||||
filterCanonicalUnifiedResources(allResourcesEntry.resources, query, localFilter) ?? [];
|
||||
entry.policyPosture = allResourcesEntry.policyPosture;
|
||||
entry.aggregations = allResourcesEntry.aggregations;
|
||||
entry.hasSnapshot = true;
|
||||
@@ -1370,8 +1423,8 @@ export function useUnifiedResources(options?: UseUnifiedResourcesOptions) {
|
||||
const initialHydration = options?.initialHydration ?? 'immediate';
|
||||
const enabled = options?.enabled ?? (() => true);
|
||||
const realtimeEnabled = options?.realtimeEnabled ?? enabled;
|
||||
const typeFilter = parseUnifiedResourcesTypeFilter(query);
|
||||
const supportsCanonicalWsHydration = query === '' || typeFilter !== null;
|
||||
const localFilter = parseUnifiedResourcesLocalFilter(query);
|
||||
const supportsCanonicalWsHydration = query === '' || localFilter !== null;
|
||||
const prefersWsInitialHydration =
|
||||
(initialHydration === 'prefer-ws' || initialHydration === 'prefer-ws-then-rest') &&
|
||||
supportsCanonicalWsHydration;
|
||||
@@ -1763,7 +1816,7 @@ export function useUnifiedResources(options?: UseUnifiedResourcesOptions) {
|
||||
const projectedResources = filterCanonicalUnifiedResources(
|
||||
mergedWsResources,
|
||||
query,
|
||||
typeFilter,
|
||||
localFilter,
|
||||
);
|
||||
const now = Date.now();
|
||||
clearInitialHydrationTimeout();
|
||||
|
||||
Reference in New Issue
Block a user