mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-10 00:14:38 +00:00
Fix Docker workload host scoping
This commit is contained in:
parent
870958ef2a
commit
410a131a3b
8 changed files with 358 additions and 12 deletions
|
|
@ -190,6 +190,7 @@ regression protection.
|
|||
13. Extend dashboard guest metadata cache persistence, metadata refresh, org-scope switching, and optimistic custom-URL updates through `frontend-modern/src/components/Dashboard/useDashboardGuestMetadataState.ts` rather than rebuilding dashboard-local storage caches, event listeners, or guest metadata API wiring inside `frontend-modern/src/components/Dashboard/useDashboardState.ts`
|
||||
14. Extend dashboard deep-link selection and hovered-row continuity semantics through `frontend-modern/src/components/Dashboard/dashboardSelectionModel.ts`, and extend table scroll preservation plus reactive selection state through `frontend-modern/src/components/Dashboard/useDashboardSelectionState.ts`, rather than rebuilding resource-query parsing, selected-row scroll pinning, or hovered-row invalidation inside `frontend-modern/src/components/Dashboard/useDashboardState.ts`; canonical typed workload IDs such as `app-container:<host>:<provider-id>` must remain exact route/selection keys and must not be reinterpreted into synthetic node scopes
|
||||
15. Extend dashboard workload route ownership, route-driven option catalogs, and toolbar filter config through `frontend-modern/src/components/Dashboard/useDashboardWorkloadRouteState.ts`, `frontend-modern/src/components/Dashboard/useDashboardWorkloadFilterOptions.ts`, `frontend-modern/src/components/Dashboard/dashboardWorkloadRouteModel.ts`, `frontend-modern/src/components/Dashboard/dashboardWorkloadFilterConfigModel.ts`, and `frontend-modern/src/components/Dashboard/dashboardWorkloadRouteStateModel.ts`, and extend query-param synchronization plus managed workload URL semantics through `frontend-modern/src/components/Dashboard/useDashboardWorkloadUrlSync.ts` and `frontend-modern/src/components/Dashboard/dashboardWorkloadUrlSyncModel.ts`, rather than rebuilding route sync, alias parsing, option derivation, toolbar callback/config wiring, reset policy, node-selection compatibility rules, param precedence, or managed workload URLs inside `frontend-modern/src/components/Dashboard/useDashboardState.ts`
|
||||
Workloads route host scopes must resolve through `frontend-modern/src/components/Dashboard/workloadTopology.ts`: app-container scopes use the canonical Docker/runtime host id before host labels or node fallbacks, while VM and system-container scopes keep the instance-node key. Route `agent` filters and option catalogs must consume that shared scope so Infrastructure related-workload links cannot drift from Workloads filtering.
|
||||
16. Extend grouped dashboard workload derivation, summary fallbacks, and grouped/windowed table presentation through `frontend-modern/src/components/Dashboard/useDashboardWorkloadDerivedState.ts`, extend viewport-driven grouped table synchronization through `frontend-modern/src/components/Dashboard/useDashboardWorkloadViewportSync.ts`, and extend node parent mapping through `frontend-modern/src/components/Dashboard/workloadTopology.ts`, rather than rebuilding grouped selectors, summary snapshot math, scroll listeners, or topology lookups inside `frontend-modern/src/components/Dashboard/useDashboardState.ts`
|
||||
17. Extend dashboard control defaults, persistent view preferences, keyboard reset behavior, column-visibility ownership, and tag-search flow through `frontend-modern/src/components/Dashboard/useDashboardControlsState.ts` and `frontend-modern/src/components/Dashboard/dashboardFilterModel.ts` rather than rebuilding sort/search/grouping state, reset drift, or column-toggle plumbing inside `frontend-modern/src/components/Dashboard/useDashboardState.ts`
|
||||
18. Extend dashboard filter active-count, reset semantics, and mobile toolbar state through `frontend-modern/src/components/Dashboard/dashboardFilterModel.ts` and `frontend-modern/src/components/Dashboard/useDashboardFilterState.ts`, rather than rebuilding filter-local state inside `frontend-modern/src/components/Dashboard/DashboardFilter.tsx`
|
||||
|
|
|
|||
|
|
@ -61,6 +61,44 @@ describe('dashboardWorkloadRouteModel', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('builds app-container host options from Docker host ids and host labels', () => {
|
||||
const options = buildDashboardWorkloadNodeOptions([
|
||||
makeGuest({
|
||||
id: 'docker-a',
|
||||
type: 'app-container',
|
||||
workloadType: 'app-container',
|
||||
node: '',
|
||||
instance: '',
|
||||
contextLabel: 'tower.local',
|
||||
dockerHostId: 'docker-host-1',
|
||||
}),
|
||||
makeGuest({
|
||||
id: 'docker-b',
|
||||
type: 'app-container',
|
||||
workloadType: 'app-container',
|
||||
node: '',
|
||||
instance: '',
|
||||
contextLabel: 'tower.local',
|
||||
dockerHostId: 'docker-host-1',
|
||||
}),
|
||||
makeGuest({
|
||||
id: 'truenas-nextcloud',
|
||||
type: 'app-container',
|
||||
workloadType: 'app-container',
|
||||
node: '',
|
||||
instance: 'nextcloud',
|
||||
contextLabel: 'truenas-main',
|
||||
dockerHostId: '',
|
||||
platformType: 'truenas',
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(options).toEqual([
|
||||
{ value: 'docker-host-1', label: 'tower.local' },
|
||||
{ value: 'truenas-main', label: 'truenas-main' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('builds kubernetes context and namespace options from canonical pod scope', () => {
|
||||
const guests = [
|
||||
makeGuest({
|
||||
|
|
|
|||
|
|
@ -248,6 +248,67 @@ describe('workloadSelectors', () => {
|
|||
|
||||
expect(result.map((guest) => guest.name)).toEqual(['nextcloud']);
|
||||
});
|
||||
|
||||
it('filters app containers by Docker host id from related-workload links', () => {
|
||||
const guests = [
|
||||
makeGuest(1, {
|
||||
name: 'grafana',
|
||||
type: 'app-container',
|
||||
workloadType: 'app-container',
|
||||
platformType: 'docker',
|
||||
dockerHostId: 'docker-host-1',
|
||||
contextLabel: 'tower.local',
|
||||
node: '',
|
||||
instance: '',
|
||||
}),
|
||||
makeGuest(2, {
|
||||
name: 'prometheus',
|
||||
type: 'app-container',
|
||||
workloadType: 'app-container',
|
||||
platformType: 'docker',
|
||||
dockerHostId: 'docker-host-1',
|
||||
contextLabel: 'tower.local',
|
||||
node: '',
|
||||
instance: '',
|
||||
}),
|
||||
makeGuest(3, {
|
||||
name: 'redis',
|
||||
type: 'app-container',
|
||||
workloadType: 'app-container',
|
||||
platformType: 'docker',
|
||||
dockerHostId: 'docker-host-2',
|
||||
contextLabel: 'edge.local',
|
||||
node: '',
|
||||
instance: '',
|
||||
}),
|
||||
];
|
||||
|
||||
const byCanonicalHostScope = filterWorkloads({
|
||||
guests,
|
||||
viewMode: 'app-container',
|
||||
statusMode: 'all',
|
||||
searchTerm: '',
|
||||
selectedNode: 'docker-host-1',
|
||||
selectedHostHint: null,
|
||||
selectedPlatform: 'docker',
|
||||
selectedKubernetesContext: null,
|
||||
});
|
||||
|
||||
expect(byCanonicalHostScope.map((guest) => guest.name)).toEqual(['grafana', 'prometheus']);
|
||||
|
||||
const byRouteHostHint = filterWorkloads({
|
||||
guests,
|
||||
viewMode: 'app-container',
|
||||
statusMode: 'all',
|
||||
searchTerm: '',
|
||||
selectedNode: null,
|
||||
selectedHostHint: 'docker-host-2',
|
||||
selectedPlatform: 'docker',
|
||||
selectedKubernetesContext: null,
|
||||
});
|
||||
|
||||
expect(byRouteHostHint.map((guest) => guest.name)).toEqual(['redis']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createWorkloadSortComparator', () => {
|
||||
|
|
|
|||
|
|
@ -157,11 +157,21 @@ describe('workloadTopology', () => {
|
|||
node: 'truenas-main',
|
||||
instance: 'truenas-main',
|
||||
});
|
||||
const appContainerWithHostLabel = makeGuest(4, {
|
||||
type: 'app-container',
|
||||
workloadType: 'app-container',
|
||||
platformType: 'docker',
|
||||
dockerHostId: '',
|
||||
contextLabel: 'tower.local',
|
||||
node: 'node-c',
|
||||
instance: 'inst-c',
|
||||
});
|
||||
expect(getWorkloadDockerHostId(dockerWithHostId)).toBe('docker-host-1');
|
||||
expect(getWorkloadDockerHostId(dockerFallback)).toBe('');
|
||||
expect(getWorkloadDockerHostId(truenasApp)).toBe('');
|
||||
expect(getWorkloadContainerHostId(dockerFallback)).toBe('node-b');
|
||||
expect(getWorkloadContainerHostId(truenasApp)).toBe('truenas-main');
|
||||
expect(getWorkloadContainerHostId(appContainerWithHostLabel)).toBe('tower.local');
|
||||
});
|
||||
|
||||
it('maps discovery host and resource IDs for app-container, pod, and vm', () => {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@ import type { WorkloadGuest, ViewMode } from '@/types/workloads';
|
|||
import { normalizeWorkloadViewModeParam, resolveWorkloadType } from '@/utils/workloads';
|
||||
import { buildSourcePlatformOptions } from '@/utils/sourcePlatformOptions';
|
||||
import type { DashboardFilterSelectOption } from './dashboardFilterModel';
|
||||
import { getKubernetesContextKey, workloadNodeScopeId } from './workloadTopology';
|
||||
import {
|
||||
getKubernetesContextKey,
|
||||
getWorkloadHostLabel,
|
||||
workloadHostScopeId,
|
||||
} from './workloadTopology';
|
||||
|
||||
export type DashboardWorkloadNodeOption = DashboardFilterSelectOption;
|
||||
|
||||
|
|
@ -15,27 +19,42 @@ export const buildDashboardWorkloadNodeOptions = (
|
|||
guests: WorkloadGuest[],
|
||||
): DashboardWorkloadNodeOption[] => {
|
||||
const labelsByScope = new Map<string, string>();
|
||||
const nodeNameCounts = new Map<string, number>();
|
||||
const scopesByLabel = new Map<string, Set<string>>();
|
||||
|
||||
for (const guest of guests) {
|
||||
const type = resolveWorkloadType(guest);
|
||||
if (type === 'pod') continue;
|
||||
const scope = workloadNodeScopeId(guest);
|
||||
const scope = workloadHostScopeId(guest);
|
||||
if (!scope || scope === '-') continue;
|
||||
const nodeName = (guest.node || '').trim();
|
||||
if (!nodeName) continue;
|
||||
nodeNameCounts.set(nodeName, (nodeNameCounts.get(nodeName) || 0) + 1);
|
||||
const label = getWorkloadHostLabel(guest);
|
||||
if (!label) continue;
|
||||
const disambiguationLabel = type === 'app-container' ? label : nodeName;
|
||||
if (!disambiguationLabel) continue;
|
||||
const scopes = scopesByLabel.get(disambiguationLabel) ?? new Set<string>();
|
||||
scopes.add(scope);
|
||||
scopesByLabel.set(disambiguationLabel, scopes);
|
||||
}
|
||||
|
||||
for (const guest of guests) {
|
||||
const type = resolveWorkloadType(guest);
|
||||
if (type === 'pod') continue;
|
||||
const scope = workloadNodeScopeId(guest);
|
||||
const scope = workloadHostScopeId(guest);
|
||||
if (!scope || scope === '-' || labelsByScope.has(scope)) continue;
|
||||
if (type === 'app-container') {
|
||||
const hostLabel = getWorkloadHostLabel(guest);
|
||||
if (!hostLabel) continue;
|
||||
const hasDuplicateHostLabel = (scopesByLabel.get(hostLabel)?.size ?? 0) > 1;
|
||||
labelsByScope.set(
|
||||
scope,
|
||||
hasDuplicateHostLabel && scope !== hostLabel ? `${hostLabel} (${scope})` : hostLabel,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const nodeName = (guest.node || '').trim();
|
||||
const instance = (guest.instance || '').trim();
|
||||
if (!nodeName) continue;
|
||||
const hasDuplicateNodeName = (nodeNameCounts.get(nodeName) || 0) > 1;
|
||||
const hasDuplicateNodeName = (scopesByLabel.get(nodeName)?.size ?? 0) > 1;
|
||||
const label = hasDuplicateNodeName && instance ? `${nodeName} (${instance})` : nodeName;
|
||||
labelsByScope.set(scope, label);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,12 @@ import { DEGRADED_HEALTH_STATUSES, OFFLINE_HEALTH_STATUSES } from '@/utils/statu
|
|||
import { getNodeDisplayName } from '@/utils/nodes';
|
||||
import { getCanonicalWorkloadId, resolveWorkloadType } from '@/utils/workloads';
|
||||
import { getWorkloadTypePresentation } from '@/utils/workloadTypePresentation';
|
||||
import { buildNodeByInstance, getKubernetesContextKey, workloadNodeScopeId } from './workloadTopology';
|
||||
import {
|
||||
buildNodeByInstance,
|
||||
getKubernetesContextKey,
|
||||
getWorkloadHostHintCandidates,
|
||||
workloadHostScopeId,
|
||||
} from './workloadTopology';
|
||||
|
||||
export interface FilterWorkloadsParams {
|
||||
guests: WorkloadGuest[];
|
||||
|
|
@ -55,15 +60,16 @@ export const filterWorkloads = ({
|
|||
|
||||
const nodeScope = selectedNode;
|
||||
if (nodeScope && viewMode !== 'pod') {
|
||||
guests = guests.filter((g) => workloadNodeScopeId(g) === nodeScope);
|
||||
guests = guests.filter((g) => workloadHostScopeId(g) === nodeScope);
|
||||
}
|
||||
|
||||
const hostHint = (selectedHostHint || '').trim().toLowerCase();
|
||||
if (!nodeScope && hostHint && viewMode !== 'pod') {
|
||||
guests = guests.filter((g) => {
|
||||
if (resolveWorkloadType(g) === 'pod') return false;
|
||||
const candidates = [g.node, g.instance, g.contextLabel];
|
||||
return candidates.some((candidate) => (candidate || '').toLowerCase().includes(hostHint));
|
||||
return getWorkloadHostHintCandidates(g).some((candidate) =>
|
||||
candidate.toLowerCase().includes(hostHint),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,28 @@ import {
|
|||
resolveWorkloadType,
|
||||
} from '@/utils/workloads';
|
||||
|
||||
const firstTrimmed = (values: Array<string | null | undefined>): string => {
|
||||
for (const value of values) {
|
||||
const trimmed = (value || '').trim();
|
||||
if (trimmed) return trimmed;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const dedupeTrimmed = (values: Array<string | null | undefined>): string[] => {
|
||||
const seen = new Set<string>();
|
||||
const result: string[] = [];
|
||||
for (const value of values) {
|
||||
const trimmed = (value || '').trim();
|
||||
if (!trimmed) continue;
|
||||
const key = trimmed.toLowerCase();
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
result.push(trimmed);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const workloadNodeScopeId = (guest: WorkloadGuest): string =>
|
||||
`${(guest.instance || '').trim()}-${(guest.node || '').trim()}`;
|
||||
|
||||
|
|
@ -29,7 +51,31 @@ export const getWorkloadDockerHostId = (guest: WorkloadGuest): string => {
|
|||
export const getWorkloadContainerHostId = (guest: WorkloadGuest): string => {
|
||||
const type = resolveWorkloadType(guest);
|
||||
if (type !== 'app-container') return '';
|
||||
return (guest.dockerHostId || guest.node || guest.instance || '').trim();
|
||||
return firstTrimmed([guest.dockerHostId, guest.contextLabel, guest.node, guest.instance]);
|
||||
};
|
||||
|
||||
export const workloadHostScopeId = (guest: WorkloadGuest): string => {
|
||||
const type = resolveWorkloadType(guest);
|
||||
if (type === 'pod') return '';
|
||||
if (type === 'app-container') return getWorkloadContainerHostId(guest);
|
||||
return workloadNodeScopeId(guest);
|
||||
};
|
||||
|
||||
export const getWorkloadHostLabel = (guest: WorkloadGuest): string => {
|
||||
const type = resolveWorkloadType(guest);
|
||||
if (type === 'app-container') {
|
||||
return firstTrimmed([guest.contextLabel, guest.node, guest.instance, guest.dockerHostId]);
|
||||
}
|
||||
return (guest.node || '').trim();
|
||||
};
|
||||
|
||||
export const getWorkloadHostHintCandidates = (guest: WorkloadGuest): string[] => {
|
||||
const type = resolveWorkloadType(guest);
|
||||
if (type === 'pod') return [];
|
||||
if (type === 'app-container') {
|
||||
return dedupeTrimmed([guest.dockerHostId, guest.contextLabel, guest.node, guest.instance]);
|
||||
}
|
||||
return dedupeTrimmed([guest.node, guest.instance, guest.contextLabel]);
|
||||
};
|
||||
|
||||
export const getDiscoveryHostIdForWorkload = (guest: WorkloadGuest): string => {
|
||||
|
|
|
|||
165
tests/integration/tests/72-docker-workloads-host-filter.spec.ts
Normal file
165
tests/integration/tests/72-docker-workloads-host-filter.spec.ts
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { test as base, expect } from '@playwright/test';
|
||||
import { createAuthenticatedStorageState } from './helpers';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
type WorkerFixtures = {
|
||||
authStorageStatePath: string;
|
||||
};
|
||||
|
||||
const SCREENSHOT_PATH = '/tmp/docker-workloads-host-filter.png';
|
||||
|
||||
const test = base.extend<{}, WorkerFixtures>({
|
||||
storageState: async ({ authStorageStatePath }, use) => {
|
||||
await use(authStorageStatePath);
|
||||
},
|
||||
authStorageStatePath: [
|
||||
async ({ browser }, use, workerInfo) => {
|
||||
const storageStatePath = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'tmp',
|
||||
'playwright-auth',
|
||||
`docker-workloads-host-filter-${workerInfo.project.name}.json`,
|
||||
);
|
||||
fs.mkdirSync(path.dirname(storageStatePath), { recursive: true });
|
||||
await createAuthenticatedStorageState(browser, storageStatePath);
|
||||
try {
|
||||
await use(storageStatePath);
|
||||
} finally {
|
||||
fs.rmSync(storageStatePath, { force: true });
|
||||
}
|
||||
},
|
||||
{ scope: 'worker' },
|
||||
],
|
||||
});
|
||||
|
||||
test.describe('Docker workloads host filter', () => {
|
||||
test.setTimeout(180_000);
|
||||
|
||||
test('keeps Docker related-workload links scoped to the selected runtime host', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.route('**/api/resources**', async (route) => {
|
||||
const requestUrl = new URL(route.request().url());
|
||||
if (
|
||||
requestUrl.pathname !== '/api/resources' ||
|
||||
requestUrl.searchParams.get('type') !== 'vm,system-container,app-container,pod'
|
||||
) {
|
||||
await route.continue();
|
||||
return;
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
data: [
|
||||
{
|
||||
id: 'app-container:docker-host-1:grafana',
|
||||
type: 'app-container',
|
||||
name: 'grafana',
|
||||
status: 'running',
|
||||
lastSeen: '2026-04-28T12:00:00Z',
|
||||
sources: ['docker'],
|
||||
parentName: 'tower.local',
|
||||
metrics: {
|
||||
cpu: { value: 8, percent: 8 },
|
||||
memory: { total: 4 * 1024 * 1024 * 1024, used: 1 * 1024 * 1024 * 1024 },
|
||||
disk: { total: 40 * 1024 * 1024 * 1024, used: 10 * 1024 * 1024 * 1024 },
|
||||
netIn: { value: 1024 },
|
||||
netOut: { value: 512 },
|
||||
diskRead: { value: 128 },
|
||||
diskWrite: { value: 64 },
|
||||
},
|
||||
docker: {
|
||||
containerId: 'docker-grafana',
|
||||
hostname: 'tower.local',
|
||||
imageName: 'grafana:11',
|
||||
runtime: 'docker',
|
||||
hostSourceId: 'docker-host-1',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'app-container:docker-host-1:prometheus',
|
||||
type: 'app-container',
|
||||
name: 'prometheus',
|
||||
status: 'running',
|
||||
lastSeen: '2026-04-28T12:00:00Z',
|
||||
sources: ['docker'],
|
||||
parentName: 'tower.local',
|
||||
metrics: {
|
||||
cpu: { value: 10, percent: 10 },
|
||||
memory: { total: 8 * 1024 * 1024 * 1024, used: 2 * 1024 * 1024 * 1024 },
|
||||
disk: { total: 80 * 1024 * 1024 * 1024, used: 20 * 1024 * 1024 * 1024 },
|
||||
netIn: { value: 2048 },
|
||||
netOut: { value: 1024 },
|
||||
diskRead: { value: 256 },
|
||||
diskWrite: { value: 128 },
|
||||
},
|
||||
docker: {
|
||||
containerId: 'docker-prometheus',
|
||||
hostname: 'tower.local',
|
||||
imageName: 'prom/prometheus:latest',
|
||||
runtime: 'podman',
|
||||
hostSourceId: 'docker-host-1',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'app-container:docker-host-2:redis',
|
||||
type: 'app-container',
|
||||
name: 'redis',
|
||||
status: 'running',
|
||||
lastSeen: '2026-04-28T12:00:00Z',
|
||||
sources: ['docker'],
|
||||
parentName: 'edge.local',
|
||||
metrics: {
|
||||
cpu: { value: 4, percent: 4 },
|
||||
memory: { total: 2 * 1024 * 1024 * 1024, used: 512 * 1024 * 1024 },
|
||||
disk: { total: 20 * 1024 * 1024 * 1024, used: 5 * 1024 * 1024 * 1024 },
|
||||
netIn: { value: 512 },
|
||||
netOut: { value: 256 },
|
||||
diskRead: { value: 64 },
|
||||
diskWrite: { value: 32 },
|
||||
},
|
||||
docker: {
|
||||
containerId: 'docker-redis',
|
||||
hostname: 'edge.local',
|
||||
imageName: 'redis:7',
|
||||
runtime: 'docker',
|
||||
hostSourceId: 'docker-host-2',
|
||||
},
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
page: 1,
|
||||
limit: 200,
|
||||
total: 3,
|
||||
totalPages: 1,
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/workloads?type=app-container&platform=docker&agent=docker-host-1', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
|
||||
await page.waitForURL(/\/workloads\?type=app-container&platform=docker&agent=docker-host-1/);
|
||||
await expect(page.locator('#dashboard-type-filter')).toHaveValue('app-container');
|
||||
await expect(page.locator('#workloads-platform-filter')).toHaveValue('docker');
|
||||
await expect(page.locator('#workloads-node-filter')).toHaveValue('docker-host-1');
|
||||
|
||||
const workloadTable = page.locator('table').first();
|
||||
await expect(workloadTable).toContainText('grafana');
|
||||
await expect(workloadTable).toContainText('prometheus');
|
||||
await expect(workloadTable).not.toContainText('redis');
|
||||
await expect(page.getByText('No guests found')).toHaveCount(0);
|
||||
|
||||
await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true });
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue