mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
@@ -549,7 +549,11 @@ ownership,
|
||||
config normalization, factory defaults, docker-gap validation, and save-payload
|
||||
serialization,
|
||||
`frontend-modern/src/features/alerts/alertOverridesModel.ts` for raw override
|
||||
normalization plus resource-backed override projection, and
|
||||
normalization plus resource-backed override projection. That override owner
|
||||
must canonicalize legacy per-node shared-storage keys onto the current
|
||||
cluster-scoped storage resource id before the thresholds surface rebuilds,
|
||||
so old Ceph/shared-datastore overrides still surface on the live v6 editor
|
||||
instead of disappearing after the feature-shell migration, and
|
||||
`frontend-modern/src/features/alerts/useAlertOverridesState.ts` for reactive
|
||||
override state, derived resource lists, and overview handoff, and
|
||||
`frontend-modern/src/features/alerts/alertDestinationsModel.ts` for email and
|
||||
|
||||
@@ -672,6 +672,13 @@ keeps pool-growth label/tone formatting inside the shared feature presentation
|
||||
layer. The storage page must keep reusing those shared owners instead of
|
||||
rebuilding storage-history timers or byte-delta formatting inside row
|
||||
components.
|
||||
That same shared alerts feature boundary now also owns legacy shared-storage
|
||||
override migration. `frontend-modern/src/features/alerts/alertOverridesModel.ts`
|
||||
and `frontend-modern/src/features/alerts/useAlertOverridesState.ts` must
|
||||
canonicalize per-node shared-storage override keys such as
|
||||
`Main-pve1-ceph-pool` onto the current cluster-scoped storage resource id
|
||||
before the thresholds table derives rows, so old Ceph override records survive
|
||||
the v6 feature-shell path instead of silently disappearing from the live editor.
|
||||
|
||||
The frontend already has several guardrail tests. The next step is to keep
|
||||
turning repeated local patterns into explicit shared primitives with hard usage
|
||||
@@ -2036,7 +2043,11 @@ ownership,
|
||||
`frontend-modern/src/features/alerts/alertsConfigurationModel.ts` for config
|
||||
normalization, factory defaults, docker-gap validation, and payload
|
||||
serialization, `frontend-modern/src/features/alerts/alertOverridesModel.ts`
|
||||
for override normalization and resource-backed projection, and
|
||||
for override normalization and resource-backed projection. That shared
|
||||
feature-model boundary must also canonicalize legacy shared-storage override
|
||||
keys onto the current storage resource id before thresholds rows are derived,
|
||||
so migrated Ceph/shared-datastore overrides survive the feature-shell path
|
||||
instead of dropping out of the live editor, and
|
||||
`frontend-modern/src/features/alerts/useAlertOverridesState.ts`
|
||||
for reactive override state and thresholds-facing resource selectors, and
|
||||
`frontend-modern/src/features/alerts/alertDestinationsModel.ts` for
|
||||
|
||||
@@ -31,6 +31,40 @@ describe('alertOverridesModel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('normalizes legacy shared-storage override ids into canonical storage resource ids', () => {
|
||||
const storage = makeResource({
|
||||
id: 'Main-cluster-ceph-pool',
|
||||
name: 'ceph-pool',
|
||||
type: 'storage',
|
||||
platformId: 'Main',
|
||||
proxmox: {
|
||||
instance: 'Main',
|
||||
node: 'cluster',
|
||||
},
|
||||
storage: {
|
||||
shared: true,
|
||||
isCeph: true,
|
||||
nodes: ['pve1', 'pve2'],
|
||||
type: 'rbd',
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
normalizeRawOverridesConfig(
|
||||
{
|
||||
'Main-pve1-ceph-pool': {
|
||||
usage: { trigger: 92, clear: 82 },
|
||||
} as any,
|
||||
},
|
||||
[storage],
|
||||
),
|
||||
).toEqual({
|
||||
'Main-cluster-ceph-pool': {
|
||||
usage: { trigger: 92, clear: 82 },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('projects guest overrides without requiring agent-backed resources', () => {
|
||||
const guest = makeResource({
|
||||
id: 'cluster-a:node-2:100',
|
||||
@@ -172,4 +206,55 @@ describe('alertOverridesModel', () => {
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('projects shared-storage overrides through the canonical storage resource id', () => {
|
||||
const storage = makeResource({
|
||||
id: 'Main-cluster-ceph-pool',
|
||||
name: 'ceph-pool',
|
||||
displayName: 'ceph-pool',
|
||||
type: 'storage',
|
||||
platformId: 'Main',
|
||||
proxmox: {
|
||||
instance: 'Main',
|
||||
node: 'cluster',
|
||||
},
|
||||
storage: {
|
||||
shared: true,
|
||||
isCeph: true,
|
||||
nodes: ['pve1', 'pve2'],
|
||||
type: 'rbd',
|
||||
},
|
||||
platformData: {
|
||||
node: 'cluster',
|
||||
instance: 'Main',
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
buildProjectedOverrides({
|
||||
rawConfig: {
|
||||
'Main-pve1-ceph-pool': {
|
||||
usage: { trigger: 92, clear: 82 },
|
||||
} as any,
|
||||
},
|
||||
nodeResources: [],
|
||||
vmResources: [],
|
||||
containerResources: [],
|
||||
storageResources: [storage],
|
||||
agentResourceList: [],
|
||||
containerRuntimeResources: [],
|
||||
getChildren: () => [],
|
||||
pbsInstanceById: new Map(),
|
||||
}),
|
||||
).toEqual([
|
||||
expect.objectContaining({
|
||||
id: 'Main-cluster-ceph-pool',
|
||||
type: 'storage',
|
||||
name: 'ceph-pool',
|
||||
thresholds: {
|
||||
usage: 92,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -123,6 +123,62 @@ describe('useAlertOverridesState', () => {
|
||||
expect(overviewOverrides()).toEqual([]);
|
||||
});
|
||||
|
||||
it('canonicalizes shared-storage overrides for the live thresholds surface', async () => {
|
||||
const [hasUnsavedChanges] = createSignal(false);
|
||||
const [overviewOverrides, setOverviewOverrides] = createSignal([]);
|
||||
const resources = [
|
||||
makeResource({
|
||||
id: 'Main-cluster-ceph-pool',
|
||||
name: 'ceph-pool',
|
||||
displayName: 'ceph-pool',
|
||||
type: 'storage',
|
||||
platformId: 'Main',
|
||||
proxmox: {
|
||||
instance: 'Main',
|
||||
node: 'cluster',
|
||||
},
|
||||
storage: {
|
||||
shared: true,
|
||||
isCeph: true,
|
||||
nodes: ['pve1', 'pve2'],
|
||||
type: 'rbd',
|
||||
},
|
||||
platformData: {
|
||||
node: 'cluster',
|
||||
instance: 'Main',
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAlertOverridesState({
|
||||
allResources: () => resources,
|
||||
byType: (resourceType) => resources.filter((resource) => resource.type === resourceType),
|
||||
children: () => [],
|
||||
hasUnsavedChanges,
|
||||
setOverviewOverrides,
|
||||
}),
|
||||
);
|
||||
|
||||
result.replaceRawOverridesConfig({
|
||||
'Main-pve1-ceph-pool': {
|
||||
usage: { trigger: 92, clear: 82 },
|
||||
} as any,
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.overrides()).toHaveLength(1));
|
||||
|
||||
expect(Object.keys(result.rawOverridesConfig())).toEqual(['Main-cluster-ceph-pool']);
|
||||
expect(result.overrides()[0]).toMatchObject({
|
||||
id: 'Main-cluster-ceph-pool',
|
||||
type: 'storage',
|
||||
thresholds: {
|
||||
usage: 92,
|
||||
},
|
||||
});
|
||||
expect(overviewOverrides()).toEqual(result.overrides());
|
||||
});
|
||||
|
||||
it('exposes canonical container runtimes for TrueNAS-backed app workloads', async () => {
|
||||
const [hasUnsavedChanges] = createSignal(false);
|
||||
const [, setOverviewOverrides] = createSignal([]);
|
||||
|
||||
@@ -38,11 +38,45 @@ const uniqueIds = (...values: unknown[]): string[] => {
|
||||
return ids;
|
||||
};
|
||||
|
||||
const buildSharedStorageLegacyKeyMap = (storageResources: Resource[]): Map<string, string> => {
|
||||
const legacyToCanonical = new Map<string, string>();
|
||||
|
||||
storageResources.forEach((resource) => {
|
||||
const storageMeta = resource.storage;
|
||||
const resourceName = asString(resource.name);
|
||||
const proxmox = resource.proxmox;
|
||||
const instance = asString(proxmox?.instance) || asString(resource.platformId);
|
||||
const canonicalID = asString(resource.id);
|
||||
|
||||
if (!storageMeta?.shared || !resourceName || !canonicalID) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clusterNodes = Array.isArray(storageMeta.nodes) ? storageMeta.nodes : [];
|
||||
clusterNodes.forEach((node) => {
|
||||
const normalizedNode = asString(node);
|
||||
if (!normalizedNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const prefix =
|
||||
instance && !normalizedNode.toLowerCase().startsWith(`${instance.toLowerCase()}-`)
|
||||
? `${instance}-${normalizedNode}`
|
||||
: normalizedNode;
|
||||
legacyToCanonical.set(`${prefix}-${resourceName}`, canonicalID);
|
||||
});
|
||||
});
|
||||
|
||||
return legacyToCanonical;
|
||||
};
|
||||
|
||||
export const normalizeRawOverridesConfig = (
|
||||
rawOverrides: Record<string, RawOverrideConfig>,
|
||||
storageResources: Resource[] = [],
|
||||
): Record<string, RawOverrideConfig> => {
|
||||
const cleanedOverrides: Record<string, RawOverrideConfig> = {};
|
||||
const priorityByKey = new Map<string, number>();
|
||||
const sharedStorageLegacyKeyMap = buildSharedStorageLegacyKeyMap(storageResources);
|
||||
|
||||
for (const [key, value] of Object.entries(rawOverrides)) {
|
||||
const normalizedGuestKey = normalizeGuestOverrideKey(key);
|
||||
@@ -57,6 +91,10 @@ export const normalizeRawOverridesConfig = (
|
||||
.replace(/^-|-$/g, '') || 'unknown';
|
||||
normalizedKey = diskMatch[1] + normalized;
|
||||
}
|
||||
const sharedStorageKey = sharedStorageLegacyKeyMap.get(normalizedKey);
|
||||
if (sharedStorageKey) {
|
||||
normalizedKey = sharedStorageKey;
|
||||
}
|
||||
|
||||
const priority = normalizedKey === key ? 1 : 0;
|
||||
const existingPriority = priorityByKey.get(normalizedKey);
|
||||
@@ -165,6 +203,7 @@ export const buildProjectedOverrides = ({
|
||||
>();
|
||||
const agentMap = new Map<string, Resource>();
|
||||
const guestMap = new Map<string, Resource>();
|
||||
const storageMap = new Map<string, Resource>();
|
||||
|
||||
const upsertProjectedOverride = (override: Override) => {
|
||||
const existingIndex = overrideIndexByID.get(override.id);
|
||||
@@ -215,6 +254,20 @@ export const buildProjectedOverrides = ({
|
||||
});
|
||||
});
|
||||
|
||||
storageResources.forEach((storageResource) => {
|
||||
const canonicalID = asString(storageResource.id);
|
||||
if (canonicalID) {
|
||||
storageMap.set(canonicalID, storageResource);
|
||||
}
|
||||
});
|
||||
|
||||
buildSharedStorageLegacyKeyMap(storageResources).forEach((canonicalID, legacyID) => {
|
||||
const storageResource = storageMap.get(canonicalID);
|
||||
if (storageResource) {
|
||||
storageMap.set(legacyID, storageResource);
|
||||
}
|
||||
});
|
||||
|
||||
[...vmResources, ...containerResources].forEach((guest) => {
|
||||
guestOverrideIdCandidates(guest).forEach((candidate) => {
|
||||
guestMap.set(candidate, guest);
|
||||
@@ -361,11 +414,11 @@ export const buildProjectedOverrides = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const storage = storageResources.find((resource) => resource.id === key);
|
||||
const storage = storageMap.get(key);
|
||||
if (storage) {
|
||||
const coords = storageCoords(storage);
|
||||
upsertProjectedOverride({
|
||||
id: key,
|
||||
id: storage.id,
|
||||
name: getAlertResourceDisplayLabel(storage),
|
||||
type: 'storage',
|
||||
resourceType: 'Storage',
|
||||
|
||||
@@ -81,6 +81,15 @@ export function useAlertOverridesState(props: AlertOverridesStateProps) {
|
||||
}
|
||||
|
||||
const rawConfig = rawOverridesConfig();
|
||||
const storageResources = props
|
||||
.allResources()
|
||||
.filter((resource) => resource.type === 'storage' || resource.type === 'datastore');
|
||||
const normalizedRawConfig = normalizeRawOverridesConfig(rawConfig, storageResources);
|
||||
if (JSON.stringify(normalizedRawConfig) !== JSON.stringify(rawConfig)) {
|
||||
setRawOverridesConfig(normalizedRawConfig);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Object.keys(rawConfig).length === 0) {
|
||||
if (overrides().length > 0) {
|
||||
setOverrides([]);
|
||||
@@ -94,9 +103,6 @@ export function useAlertOverridesState(props: AlertOverridesStateProps) {
|
||||
...props.byType('system-container'),
|
||||
...props.byType('oci-container'),
|
||||
];
|
||||
const storageResources = props
|
||||
.allResources()
|
||||
.filter((resource) => resource.type === 'storage' || resource.type === 'datastore');
|
||||
const agentResourceList = agentResources();
|
||||
const overridesList = buildProjectedOverrides({
|
||||
rawConfig,
|
||||
@@ -137,7 +143,10 @@ export function useAlertOverridesState(props: AlertOverridesStateProps) {
|
||||
});
|
||||
|
||||
const replaceRawOverridesConfig = (value: Record<string, RawOverrideConfig>) => {
|
||||
setRawOverridesConfig(normalizeRawOverridesConfig(value));
|
||||
const storageResources = props
|
||||
.allResources()
|
||||
.filter((resource) => resource.type === 'storage' || resource.type === 'datastore');
|
||||
setRawOverridesConfig(normalizeRawOverridesConfig(value, storageResources));
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -88,6 +88,10 @@ import {
|
||||
normalizeMetricDelayMap,
|
||||
unifiedTypeToAlertDisplayType,
|
||||
} from '@/features/alerts/helpers';
|
||||
import {
|
||||
buildProjectedOverrides,
|
||||
normalizeRawOverridesConfig,
|
||||
} from '@/features/alerts/alertOverridesModel';
|
||||
import {
|
||||
getAlertIncidentAcknowledgedBadgeClass,
|
||||
getAlertIncidentEventFilterActionButtonClass,
|
||||
@@ -194,6 +198,69 @@ describe('alert resource display labels', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('shared storage override migration', () => {
|
||||
it('keeps legacy Ceph override ids visible on the v6 thresholds surface', () => {
|
||||
const storage = {
|
||||
id: 'Main-cluster-ceph-pool',
|
||||
name: 'ceph-pool',
|
||||
displayName: 'ceph-pool',
|
||||
type: 'storage',
|
||||
platformId: 'Main',
|
||||
proxmox: {
|
||||
instance: 'Main',
|
||||
node: 'cluster',
|
||||
},
|
||||
storage: {
|
||||
shared: true,
|
||||
isCeph: true,
|
||||
nodes: ['pve1', 'pve2'],
|
||||
type: 'rbd',
|
||||
},
|
||||
platformData: {
|
||||
instance: 'Main',
|
||||
node: 'cluster',
|
||||
},
|
||||
} as Resource;
|
||||
|
||||
const rawConfig = normalizeRawOverridesConfig(
|
||||
{
|
||||
'Main-pve1-ceph-pool': {
|
||||
usage: { trigger: 92, clear: 82 },
|
||||
} as RawOverrideConfig,
|
||||
},
|
||||
[storage],
|
||||
);
|
||||
|
||||
expect(rawConfig).toEqual({
|
||||
'Main-cluster-ceph-pool': {
|
||||
usage: { trigger: 92, clear: 82 },
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
buildProjectedOverrides({
|
||||
rawConfig,
|
||||
nodeResources: [],
|
||||
vmResources: [],
|
||||
containerResources: [],
|
||||
storageResources: [storage],
|
||||
agentResourceList: [],
|
||||
containerRuntimeResources: [],
|
||||
getChildren: () => [],
|
||||
pbsInstanceById: new Map(),
|
||||
}),
|
||||
).toEqual([
|
||||
expect.objectContaining({
|
||||
id: 'Main-cluster-ceph-pool',
|
||||
type: 'storage',
|
||||
thresholds: {
|
||||
usage: 92,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tab path helpers', () => {
|
||||
it('maps tab to path', () => {
|
||||
expect(pathForTab('overview')).toBe('/alerts/overview');
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
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 test = base.extend<{}, WorkerFixtures>({
|
||||
storageState: async ({ authStorageStatePath }, use) => {
|
||||
await use(authStorageStatePath);
|
||||
},
|
||||
authStorageStatePath: [async ({ browser }, use, workerInfo) => {
|
||||
const storageStatePath = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'tmp',
|
||||
'playwright-auth',
|
||||
`ceph-alert-thresholds-${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.use({ serviceWorkers: 'block' });
|
||||
|
||||
test.describe('Ceph alert thresholds', () => {
|
||||
test.setTimeout(180_000);
|
||||
|
||||
test('keeps shared Ceph storage visible and editable on the live thresholds surface', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.addInitScript(() => {
|
||||
class FakeWebSocket {
|
||||
static CONNECTING = 0;
|
||||
static OPEN = 1;
|
||||
static CLOSING = 2;
|
||||
static CLOSED = 3;
|
||||
|
||||
readonly url: string;
|
||||
readyState = FakeWebSocket.CLOSED;
|
||||
onopen: ((event: Event) => void) | null = null;
|
||||
onclose:
|
||||
| ((event: { code?: number; reason?: string; wasClean?: boolean }) => void)
|
||||
| null = null;
|
||||
onerror: ((event: Event) => void) | null = null;
|
||||
onmessage: ((event: MessageEvent) => void) | null = null;
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = url;
|
||||
queueMicrotask(() => {
|
||||
this.onclose?.({ code: 1006, reason: 'e2e websocket disabled', wasClean: false });
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.readyState = FakeWebSocket.CLOSED;
|
||||
}
|
||||
|
||||
send() {}
|
||||
|
||||
addEventListener() {}
|
||||
|
||||
removeEventListener() {}
|
||||
}
|
||||
|
||||
// @ts-expect-error Playwright init script runs in the browser context.
|
||||
window.WebSocket = FakeWebSocket;
|
||||
});
|
||||
|
||||
await page.context().route('**/api/resources**', async (route) => {
|
||||
const requestUrl = new URL(route.request().url());
|
||||
if (requestUrl.pathname !== '/api/resources') {
|
||||
await route.continue();
|
||||
return;
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
data: [
|
||||
{
|
||||
id: 'Main-pve1',
|
||||
type: 'agent',
|
||||
name: 'pve1',
|
||||
displayName: 'pve1',
|
||||
platformId: 'Main',
|
||||
platformType: 'proxmox-pve',
|
||||
sourceType: 'api',
|
||||
sources: ['proxmox'],
|
||||
status: 'online',
|
||||
canonicalIdentity: {
|
||||
displayName: 'pve1',
|
||||
hostname: 'pve1',
|
||||
platformId: 'Main',
|
||||
},
|
||||
proxmox: {
|
||||
node: 'pve1',
|
||||
instance: 'Main',
|
||||
},
|
||||
platformData: {
|
||||
node: 'pve1',
|
||||
instance: 'Main',
|
||||
clusterName: 'Main',
|
||||
isClusterMember: true,
|
||||
sources: ['proxmox'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Main-cluster-ceph-pool',
|
||||
type: 'storage',
|
||||
name: 'ceph-pool',
|
||||
displayName: 'ceph-pool',
|
||||
platformId: 'Main',
|
||||
platformType: 'proxmox-pve',
|
||||
sourceType: 'api',
|
||||
sources: ['proxmox'],
|
||||
status: 'available',
|
||||
canonicalIdentity: {
|
||||
displayName: 'ceph-pool',
|
||||
platformId: 'Main-cluster-ceph-pool',
|
||||
},
|
||||
storage: {
|
||||
type: 'rbd',
|
||||
shared: true,
|
||||
isCeph: true,
|
||||
nodes: ['pve1', 'pve2'],
|
||||
},
|
||||
proxmox: {
|
||||
node: 'cluster',
|
||||
instance: 'Main',
|
||||
},
|
||||
platformData: {
|
||||
node: 'cluster',
|
||||
instance: 'Main',
|
||||
sources: ['proxmox'],
|
||||
},
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
page: 1,
|
||||
limit: 200,
|
||||
total: 2,
|
||||
totalPages: 1,
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.context().route('**/api/alerts/config', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
enabled: true,
|
||||
activationState: 'active',
|
||||
storageDefault: {
|
||||
trigger: 85,
|
||||
clear: 80,
|
||||
},
|
||||
overrides: {
|
||||
'Main-pve1-ceph-pool': {
|
||||
usage: {
|
||||
trigger: 92,
|
||||
clear: 82,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.context().route('**/api/alerts/active', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify([]),
|
||||
});
|
||||
});
|
||||
|
||||
await page.context().route('**/api/notifications/email', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
enabled: false,
|
||||
provider: '',
|
||||
server: '',
|
||||
port: 587,
|
||||
username: '',
|
||||
password: '',
|
||||
from: '',
|
||||
to: [],
|
||||
tls: false,
|
||||
startTLS: false,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.context().route('**/api/notifications/apprise', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
enabled: false,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/alerts/thresholds/infrastructure', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
|
||||
await expect(page).toHaveURL(/\/alerts\/thresholds\/infrastructure/);
|
||||
await expect(page.getByRole('heading', { name: 'Alert Thresholds' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Storage Devices' })).toBeVisible();
|
||||
await expect(page.getByText('ceph-pool', { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Revert to defaults for ceph-pool' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Edit thresholds for ceph-pool' })).toBeVisible();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user