From eceb38a6bccbb7007a71ba0d33f18e50f2c1a21c Mon Sep 17 00:00:00 2001 From: Josh Wright Date: Tue, 13 Oct 2026 15:39:05 -0400 Subject: [PATCH] Remove standalone Activity log page (Refs #153) --- docs/guides/maintenance.md | 2 +- docs/guides/native-hosts.md | 4 ++ docs/guides/provisioning.md | 20 ++++++- src/lib/activity-redirect.ts | 44 ++++++++++++++ src/lib/navigation-context.ts | 6 +- src/lib/navigation.ts | 10 +--- src/routes/activity/+page.server.ts | 12 +--- src/routes/activity/+page.svelte | 23 ------- src/routes/hosts/+page.svelte | 3 +- src/routes/hosts/operations/+page.svelte | 4 +- src/routes/operations/+page.server.ts | 5 +- src/routes/servers/[name]/+page.svelte | 10 ---- tests/unit/activity-redirects.test.ts | 40 +++++++++++++ tests/unit/activity.test.ts | 26 ++++---- tests/unit/command-palette.test.ts | 18 ++++++ tests/unit/navigation-context.test.ts | 18 +++++- tests/unit/navigation.test.ts | 12 ++-- tests/unit/ui-activity.test.ts | 59 ++++++------------ tests/unit/ui-host-operation-labels.test.ts | 66 ++++++++++++++++++++- tests/unit/ui-navigation.test.ts | 6 +- tests/unit/ui-server-detail.test.ts | 4 +- 21 files changed, 260 insertions(+), 132 deletions(-) create mode 100644 src/lib/activity-redirect.ts delete mode 100644 src/routes/activity/+page.svelte create mode 100644 tests/unit/activity-redirects.test.ts diff --git a/docs/guides/maintenance.md b/docs/guides/maintenance.md index 94b4c84..d943162 100644 --- a/docs/guides/maintenance.md +++ b/docs/guides/maintenance.md @@ -17,7 +17,7 @@ Campaigns retain managed-file deployment evidence; Collections define lineups. when the running revision is unverified. - **Policies & schedules** controls health recovery, configuration drift, automatic Plutonium updates, and the optional daily restart schedule independently. -- **Activity** uses the shared Activity log, scoped to maintenance and recovery. +- **Activity** shows retained history scoped to maintenance and recovery. Filter retained findings, update results, and scheduled restarts in one timeline. Follow a finding for evidence or an operation for execution progress. Use Overview’s **Active work & attention** to cancel waiting work or retry failed diff --git a/docs/guides/native-hosts.md b/docs/guides/native-hosts.md index 702a0ad..a4e537b 100644 --- a/docs/guides/native-hosts.md +++ b/docs/guides/native-hosts.md @@ -525,6 +525,10 @@ local credentials already exist. ## Host and game-file API +Open **Fleet → Hosts → Operation history** for retained operations across all +hosts, including deleted hosts. A host's **Operation history** action scopes the +same history to that host. Filters and pagination retain the selected scope. + - `GET /api/hosts/:id`: host details and deletion blockers. - `GET /api/hosts/operations?page=1&filter=important`: operation history across hosts, newest first, with 25 summaries per page and filtered total/page counts. diff --git a/docs/guides/provisioning.md b/docs/guides/provisioning.md index 87d1538..67453f4 100644 --- a/docs/guides/provisioning.md +++ b/docs/guides/provisioning.md @@ -1,8 +1,7 @@ # Provisioning -Open **Activity → Activity log** for retained lifecycle, host, campaign, and -maintenance history. Use **Manage servers** from that page or command search -for server queues, bulk cancellation, and lifecycle actions. **Fleet → Collections** owns reusable lineups and collection launch +Open **Activity → Provisioning** for server queues, bulk cancellation, lifecycle +actions, and retained lifecycle history. **Fleet → Collections** owns reusable lineups and collection launch progress; their individual operations remain visible in Provisioning. Provisioning owns the queues, safety checks, execution, and recovery for all six actions. Create a server using **Create server**. For an existing @@ -305,6 +304,21 @@ query parameters. Collection launch links at `/provisioning/collections/:id` or query parameters. `/api/deployments` remains compatible, including old campaign detail endpoints. No history or IDs are renamed or discarded. +The standalone Activity log has been retired. Use a server's **History** tab for +its combined history, **Activity → Maintenance → Activity** for maintenance and +recovery, **Releases → Campaigns** or **Releases → Betas → History** for release +history, and **Fleet → Hosts → Operation history** for retained host operations. +Host operation history remains available after a host is deleted. + +Legacy `/activity` and `/operations` bookmarks redirect to Provisioning history +by default. Server-scoped bookmarks open the server's History tab; host-scoped +bookmarks open host operation history. Source-only bookmarks open the matching +Campaigns, Betas, Maintenance, host operations, or autoscaling provisioning +history. Shared history filters are retained when opening server or Maintenance +history; pagination is reset when switching to a different kind of history. +`GET /api/activity` and the shared history views remain available. Historical +records, event collection, and retention are unchanged. + Start and Stop use durable `instance.start` and `instance.stop` host-agent operations. Rebuild uses `instance.rebuild` and replaces the instance files. See [native host management](native-hosts.md) for command delivery and recovery. diff --git a/src/lib/activity-redirect.ts b/src/lib/activity-redirect.ts new file mode 100644 index 0000000..9a43aad --- /dev/null +++ b/src/lib/activity-redirect.ts @@ -0,0 +1,44 @@ +const historyFilters = ['host', 'type', 'source', 'state', 'days', 'q', 'limit', 'offset']; + +/** Route retired activity bookmarks to the existing history that owns their scope. */ +export function activityRedirectHref(url: URL): string { + const params = new URLSearchParams(); + const copy = (keys: string[]) => { + for (const key of keys) { + const value = url.searchParams.get(key); + if (value) params.set(key, value); + } + }; + const server = url.searchParams.get('server')?.trim(); + const host = url.searchParams.get('host')?.trim(); + const source = url.searchParams.get('source'); + let path: string; + let hash = ''; + if (server) { + path = `/servers/${encodeURIComponent(server)}`; + params.set('tab', 'history'); + copy(historyFilters); + hash = url.hash; + } else if (host || source === 'host') { + path = '/hosts/operations'; + if (host) params.set('host', host); + } else if (source === 'maintenance' || source === 'recovery') { + path = '/maintenance'; + params.set('view', 'activity'); + copy(historyFilters); + hash = url.hash; + } else if (source === 'campaign') { + path = '/campaigns'; + copy(['q']); + } else if (source === 'beta') { + path = '/betas'; + params.set('view', 'history'); + copy(['q']); + } else { + path = '/provisioning'; + params.set('view', 'history'); + if (source === 'autoscaling') params.set('source', source); + } + copy(['returnTo']); + return path + (params.size ? `?${params}` : '') + hash; +} diff --git a/src/lib/navigation-context.ts b/src/lib/navigation-context.ts index 81a14a9..f2d7a8e 100644 --- a/src/lib/navigation-context.ts +++ b/src/lib/navigation-context.ts @@ -1,3 +1,5 @@ +import { activityRedirectHref } from './activity-redirect.js'; + const origin = 'https://electric-cherry.invalid'; const pagePath = /^\/(?:$|activity|operations|autoscaling|account|bank|capacity|metrics|server-keys|settings(?:\/users)?|servers\/[^/]+|hosts(?:\/[^/]+)?|collections(?:\/launches\/[^/]+)?|(?:scripts|configs|plugins|betas|campaigns|maintenance|provisioning|matches)(?:\/[^/]+)?|game-files|leaderboard(?:\/players\/[^/]+(?:\/[^/]+)?)?)$/; @@ -16,6 +18,8 @@ export function safeReturnHref(value: string | null | undefined, fallback: strin try { const url = new URL(value, origin); if (url.origin !== origin || !pagePath.test(url.pathname)) return fallback; + if (url.pathname === '/activity' || url.pathname === '/operations') + return activityRedirectHref(url); return url.pathname + url.search + url.hash; } catch { return fallback; @@ -45,8 +49,6 @@ export function returnLabel(href: string): string { if (path.startsWith('/hosts/')) return 'Back to host'; const names: Record = { '': 'Servers', - activity: 'Activity', - operations: 'Activity', autoscaling: 'Autoscaling', collections: 'Collections', hosts: 'Hosts', diff --git a/src/lib/navigation.ts b/src/lib/navigation.ts index 1b154e2..1656d70 100644 --- a/src/lib/navigation.ts +++ b/src/lib/navigation.ts @@ -28,8 +28,7 @@ export const navigationGroups: NavigationGroup[] = [ label: 'Hosts', href: '/hosts', shortcut: 'o', - matches: (path) => - path === '/hosts' || /^\/hosts\/(?!operations(?:\/|$))/.test(path) + matches: (path) => path === '/hosts' || path.startsWith('/hosts/') }, { label: 'Capacity', href: '/capacity' }, { label: 'Autoscaling', href: '/autoscaling' }, @@ -52,13 +51,6 @@ export const navigationGroups: NavigationGroup[] = [ id: 'activity', label: 'Activity', items: [ - { - label: 'Activity log', - href: '/activity', - shortcut: 'd', - matches: (path) => - path === '/activity' || path === '/operations' || path === '/hosts/operations' - }, { label: 'Provisioning', href: '/provisioning', diff --git a/src/routes/activity/+page.server.ts b/src/routes/activity/+page.server.ts index 4aed092..607f7b7 100644 --- a/src/routes/activity/+page.server.ts +++ b/src/routes/activity/+page.server.ts @@ -1,12 +1,6 @@ -import { parseActivityQuery } from '$lib/activity.js'; -import { activityHistory } from '$lib/server/activity.js'; +import { redirect } from '@sveltejs/kit'; +import { activityRedirectHref } from '$lib/activity-redirect.js'; export function load({ url }: { url: URL }) { - return { - activity: activityHistory({ - ...parseActivityQuery(url.searchParams), - excludeProvisioning: true, - excludeMaintenance: true - }) - }; + redirect(308, activityRedirectHref(url)); } diff --git a/src/routes/activity/+page.svelte b/src/routes/activity/+page.svelte deleted file mode 100644 index e09d226..0000000 --- a/src/routes/activity/+page.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - -Activity · Electric Cherry - -
- - -
- - diff --git a/src/routes/hosts/+page.svelte b/src/routes/hosts/+page.svelte index 6074c6e..caf8ad6 100644 --- a/src/routes/hosts/+page.svelte +++ b/src/routes/hosts/+page.svelte @@ -86,7 +86,8 @@ Hosts · Electric Cherry
- {#snippet actions()}Operation history{#if canManage}{/if}{/snippet} diff --git a/src/routes/hosts/operations/+page.svelte b/src/routes/hosts/operations/+page.svelte index f22b314..150a6d2 100644 --- a/src/routes/hosts/operations/+page.svelte +++ b/src/routes/hosts/operations/+page.svelte @@ -141,7 +141,7 @@
{#snippet actions()}{/snippet} {#if data.scope.operation || $page.url.searchParams.has('returnTo')}{/if} {#if data.scope.operation}
Selected host operation{data.scope.operation}
diff --git a/src/routes/operations/+page.server.ts b/src/routes/operations/+page.server.ts index dd3e894..607f7b7 100644 --- a/src/routes/operations/+page.server.ts +++ b/src/routes/operations/+page.server.ts @@ -1,5 +1,6 @@ import { redirect } from '@sveltejs/kit'; +import { activityRedirectHref } from '$lib/activity-redirect.js'; -export function load() { - redirect(308, '/activity'); +export function load({ url }: { url: URL }) { + redirect(308, activityRedirectHref(url)); } diff --git a/src/routes/servers/[name]/+page.svelte b/src/routes/servers/[name]/+page.svelte index 8a9cf15..20f886e 100644 --- a/src/routes/servers/[name]/+page.svelte +++ b/src/routes/servers/[name]/+page.svelte @@ -837,11 +837,6 @@ {:else if activeSection === 'matches'} {#key name}{/key} {:else if activeSection === 'history'} - {#key name}{/key} {/if}
@@ -866,11 +861,6 @@ min-width: 0; scroll-margin-top: 80px; } - .history-links { - display: flex; - flex-wrap: wrap; - gap: 12px; - } .server-actions { text-align: right; } diff --git a/tests/unit/activity-redirects.test.ts b/tests/unit/activity-redirects.test.ts new file mode 100644 index 0000000..827b361 --- /dev/null +++ b/tests/unit/activity-redirects.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from 'vitest'; +import { load as activity } from '../../src/routes/activity/+page.server.js'; +import { load as operations } from '../../src/routes/operations/+page.server.js'; + +describe.each([ + ['/activity', activity], + ['/operations', operations] +] as const)('retired %s page', (path, load) => { + it.each([ + ['', '/provisioning?view=history'], + ['?source=provisioning', '/provisioning?view=history'], + ['?source=autoscaling', '/provisioning?view=history&source=autoscaling'], + ['?source=unknown&offset=100', '/provisioning?view=history'], + ['?server=town', '/servers/town?tab=history'], + ['?server=town%20%2F%201', '/servers/town%20%2F%201?tab=history'], + [ + '?server=town&host=host-1&source=campaign&state=failed&days=7&q=files&limit=10&offset=20', + '/servers/town?tab=history&host=host-1&source=campaign&state=failed&days=7&q=files&limit=10&offset=20' + ], + [ + '?server=town&tab=overview&excludeProvisioning=true&excludeMaintenance=true#operation-1', + '/servers/town?tab=history#operation-1' + ], + ['?host=host%20%2F%201&offset=50', '/hosts/operations?host=host+%2F+1'], + ['?server=%20&host=deleted-host', '/hosts/operations?host=deleted-host'], + ['?source=host', '/hosts/operations'], + ['?source=campaign&q=files&offset=50', '/campaigns?q=files'], + ['?source=beta&q=release&offset=50', '/betas?view=history&q=release'], + [ + '?source=maintenance&type=update&state=failed&offset=25', + '/maintenance?view=activity&type=update&source=maintenance&state=failed&offset=25' + ], + ['?source=recovery', '/maintenance?view=activity&source=recovery'], + ['?returnTo=%2Fhosts', '/provisioning?view=history&returnTo=%2Fhosts'] + ])('redirects %s directly to %s', (query, location) => { + expect(() => load({ url: new URL(`https://ec.test${path}${query}`) })).toThrow( + expect.objectContaining({ status: 308, location }) + ); + }); +}); diff --git a/tests/unit/activity.test.ts b/tests/unit/activity.test.ts index 6a33fca..49fff27 100644 --- a/tests/unit/activity.test.ts +++ b/tests/unit/activity.test.ts @@ -475,7 +475,7 @@ describe('unified retained Activity', () => { expect(api.activityHistory({ source: 'campaign' }).total).toBe(1); }); - it('uses the same query contract for API and page loaders with no-store API responses', async () => { + it('keeps the shared history query contract and no-store API responses', async () => { const api = await setup(); api.operation('a'); api.operation('b', { state: 'failed' }); @@ -483,23 +483,19 @@ describe('unified retained Activity', () => { api.maintenance(1, 'failed'); api.incident('finding', 'health', 'attention'); const url = new URL( - 'http://localhost/activity?state=failed&limit=1&excludeProvisioning=true&excludeMaintenance=true' + 'http://localhost/api/activity?state=failed&limit=1&excludeProvisioning=true&excludeMaintenance=true' ); const { GET } = await import('../../src/routes/api/activity/+server.js'); - const { load } = await import('../../src/routes/activity/+page.server.js'); const response = GET({ url }); expect(response.headers.get('cache-control')).toBe('no-store'); - expect(await response.json()).toEqual(JSON.parse(JSON.stringify(load({ url }).activity))); - expect(load({ url }).activity.rows[0].id).toBe('host:failed-check'); - expect( - load({ - url: new URL('http://localhost/activity?excludeProvisioning=false') - }).activity.rows.some((row) => row.id.startsWith('provisioning:')) - ).toBe(false); - expect( - load({ - url: new URL('http://localhost/activity?excludeMaintenance=false') - }).activity.rows.map((row) => row.id) - ).toEqual(['host:failed-check']); + const body = await response.json(); + expect(body).toEqual( + JSON.parse(JSON.stringify(api.activityHistory(parseActivityQuery(url.searchParams)))) + ); + expect(body.rows[0].id).toBe('host:failed-check'); + const history = await GET({ url: new URL('http://localhost/api/activity') }).json(); + expect(history.rows.map((row: { id: string }) => row.id)).toEqual( + expect.arrayContaining(['provisioning:a', 'provisioning:b', 'host:failed-check']) + ); }); }); diff --git a/tests/unit/command-palette.test.ts b/tests/unit/command-palette.test.ts index db32860..d9ffaf4 100644 --- a/tests/unit/command-palette.test.ts +++ b/tests/unit/command-palette.test.ts @@ -8,6 +8,24 @@ import { } from '../../src/lib/ui/command-palette.js'; describe('command palette destinations', () => { + it('omits the retired Activity log while retaining the pages that own history', () => { + for (const role of ['admin', 'operator', 'viewer', 'super_viewer'] as const) { + const destinations = palettePageResults(role, '').map((item) => item.href); + expect(destinations).not.toContain('/activity'); + expect(destinations).not.toContain('/operations'); + expect(palettePageResults(role, 'Activity log')).toEqual([]); + expect(destinations).toEqual( + expect.arrayContaining([ + '/provisioning', + '/maintenance', + '/campaigns', + '/betas', + '/hosts' + ]) + ); + } + }); + it('lets Super Viewers search administration pages without offering operations', () => { const results = palettePageResults('super_viewer', ''); expect(results.filter((item) => item.group === 'Actions')).toEqual([]); diff --git a/tests/unit/navigation-context.test.ts b/tests/unit/navigation-context.test.ts index 2b9034d..72a43d3 100644 --- a/tests/unit/navigation-context.test.ts +++ b/tests/unit/navigation-context.test.ts @@ -20,13 +20,25 @@ describe('navigation context', () => { expect(returnLabel('/collections?view=history&launches=30')).toBe('Back to launch history'); }); - it('retains Activity filters and a selected result when opening a detail', () => { - const source = '/activity?source=maintenance&state=failed&offset=25#operation-12'; + it('retains Maintenance filters and a selected result when opening a detail', () => { + const source = '/maintenance?view=activity&state=failed&offset=25#operation-12'; const target = new URL(contextHref('/provisioning/12', source), 'http://localhost'); expect(safeReturnHref(target.searchParams.get('returnTo'), '/')).toBe(source); - expect(returnLabel(source)).toBe('Back to Activity'); + expect(returnLabel(source)).toBe('Back to Maintenance'); }); + it.each(['/activity', '/operations'])( + 'replaces retired %s return links with their current history', + (path) => { + const source = `${path}?server=town&host=host-1&state=failed&offset=25`; + const expected = '/servers/town?tab=history&host=host-1&state=failed&offset=25'; + expect(safeReturnHref(source, '/')).toBe(expected); + const target = new URL(contextHref('/provisioning/12', source), 'http://localhost'); + expect(target.searchParams.get('returnTo')).toBe(expected); + expect(returnLabel(expected)).toBe('Back to server'); + } + ); + it('preserves the selected diagnostic tab on both sides of a contextual link', () => { const url = new URL( contextHref( diff --git a/tests/unit/navigation.test.ts b/tests/unit/navigation.test.ts index 753ba55..7020d9e 100644 --- a/tests/unit/navigation.test.ts +++ b/tests/unit/navigation.test.ts @@ -11,11 +11,10 @@ describe('application navigation', () => { ['/servers/t5_zombie_theater_1', 'fleet', '/'], ['/collections/launches/42', 'fleet', '/collections'], ['/hosts/42', 'fleet', '/hosts'], - ['/hosts/operations', 'activity', '/activity'], + ['/hosts/operations', 'fleet', '/hosts'], ['/provisioning', 'activity', '/provisioning'], ['/provisioning/job-1', 'activity', '/provisioning'], ['/deployments/job-1', 'activity', '/provisioning'], - ['/activity', 'activity', '/activity'], ['/betas/new', 'releases', '/betas'], ['/campaigns/42', 'releases', '/campaigns'], ['/configs/42', 'releases', '/configs'], @@ -33,14 +32,15 @@ describe('application navigation', () => { ).toEqual([href]); }); - it('keeps Operations reachable from Activity and redirects legacy overview links', async () => { + it('keeps Provisioning and Maintenance in Activity without the retired log or shortcut', () => { expect( navigationGroups .find((group) => group.id === 'activity') ?.items.map((item) => item.label) - ).toEqual(['Activity log', 'Provisioning', 'Maintenance']); - const { load } = await import('../../src/routes/operations/+page.server.js'); - expect(load).toThrow(expect.objectContaining({ status: 308, location: '/activity' })); + ).toEqual(['Provisioning', 'Maintenance']); + const items = navigationGroups.flatMap((group) => group.items); + expect(items.some((item) => ['/activity', '/operations'].includes(item.href))).toBe(false); + expect(items.some((item) => item.shortcut === 'd')).toBe(false); }); it('keeps administration and its shortcuts out of operator and viewer navigation', () => { diff --git a/tests/unit/ui-activity.test.ts b/tests/unit/ui-activity.test.ts index b9a88be..a5a3115 100644 --- a/tests/unit/ui-activity.test.ts +++ b/tests/unit/ui-activity.test.ts @@ -1,10 +1,8 @@ import { beforeEach, afterEach, expect, it, vi } from 'vitest'; import { render } from 'svelte/server'; -import type { ComponentProps } from 'svelte'; import { writable, type Writable } from 'svelte/store'; import { page } from '$app/stores'; import ActivityLog from '../../src/lib/ui/components/ActivityLog.svelte'; -import ActivityPage from '../../src/routes/activity/+page.svelte'; import { activityApiHref, activityHistoryHref, @@ -13,7 +11,9 @@ import { } from '../../src/lib/ui/activity-history.js'; import type { ActivityPage as ActivityReport, ActivityRow } from '../../src/lib/activity.js'; -vi.mock('$app/stores', () => ({ page: writable({ url: new URL('http://localhost/activity') }) })); +vi.mock('$app/stores', () => ({ + page: writable({ url: new URL('http://localhost/servers/town?tab=history') }) +})); vi.mock('$app/navigation', () => ({ goto: vi.fn() })); vi.mock('$ui/stores/fleet.js', () => ({ fleet: writable({ servers: [], operations: [], receivedAt: 0 }) @@ -43,7 +43,7 @@ const report = (): ActivityReport => ({ offset: 0, query: { limit: 25, offset: 0 } }); -function scope(path = '/activity') { +function scope(path = '/servers/town?tab=history') { (page as unknown as Writable<{ url: URL }>).set({ url: new URL(`http://localhost${path}`) }); } beforeEach(() => { @@ -62,7 +62,7 @@ it.each([ ['90', '90', 'Last 90 days'], ['14', '14', 'Last 14 days'] ])('selects the current date range for days=%s on desktop and mobile', (days, value, label) => { - scope(days ? `/activity?days=${days}` : '/activity'); + scope(days ? `/servers/town?tab=history&days=${days}` : '/servers/town?tab=history'); const body = render(ActivityLog, { props: { initial: report() } }).body; for (const suffix of ['', '-mobile']) { const select = body.match( @@ -88,7 +88,8 @@ it('sends only supported normalized filters and enforces scoped identities', () expect(params.has('tab')).toBe(false); expect(params.has('returnTo')).toBe(false); expect( - activityQuery(new URL('https://example.test/activity?type=unknown&days=0')).type + activityQuery(new URL('https://example.test/servers/town?tab=history&type=unknown&days=0')) + .type ).toBeUndefined(); }); @@ -112,7 +113,9 @@ it('resets pagination when filtering and preserves the containing page, hash, an }); it('keeps provisioning excluded from log polling after filtering or clearing filters', () => { - const url = new URL('http://localhost/activity?type=create&excludeProvisioning=false'); + const url = new URL( + 'http://localhost/servers/town?tab=history&type=create&excludeProvisioning=false' + ); const scope = { excludeProvisioning: true }; expect(new URL(activityApiHref(url, scope), url).searchParams.get('excludeProvisioning')).toBe( 'true' @@ -125,7 +128,9 @@ it('keeps provisioning excluded from log polling after filtering or clearing fil }); it('keeps maintenance excluded from log polling while preserving the Maintenance history scope', () => { - const url = new URL('http://localhost/activity?source=maintenance&excludeMaintenance=false'); + const url = new URL( + 'http://localhost/servers/town?tab=history&source=maintenance&excludeMaintenance=false' + ); const logScope = { excludeProvisioning: true, excludeMaintenance: true }; expect( new URL(activityApiHref(url, logScope), url).searchParams.get('excludeMaintenance') @@ -155,39 +160,8 @@ it('shows relative recent timestamps and preserves accurate older dates and inva ); }); -it('renders one page heading with provisioning management and filters removed', () => { - const body = render(ActivityPage, { - props: { - data: { - activity: report(), - user: { role: 'admin' }, - setupRequired: false - } as ComponentProps['data'] - } - }).body; - expect(body.match(/]/g)).toHaveLength(1); - expect(body).not.toContain('Manage servers'); - expect(body).not.toContain('href="/provisioning"'); - expect(body).not.toContain('