diff --git a/src/constants/ssp-section-tabs.ts b/src/constants/ssp-section-tabs.ts index ffe72eca..bcb51328 100644 --- a/src/constants/ssp-section-tabs.ts +++ b/src/constants/ssp-section-tabs.ts @@ -10,15 +10,15 @@ export interface SspSectionTab { export const SSP_SECTION_TABS: SspSectionTab[] = [ { label: 'Overview', suffix: 'overview' }, - { label: 'System Characteristics', suffix: 'characteristics' }, - { label: 'System Implementation', suffix: 'implementation' }, - { label: 'Users', suffix: 'users' }, + // { label: 'System Characteristics', suffix: 'characteristics' }, + // { label: 'System Implementation', suffix: 'implementation' }, + // { label: 'Users', suffix: 'users' }, { label: 'Components', suffix: 'components' }, - { label: 'Leveraged Authorizations', suffix: 'authorizations' }, - { label: 'Control Implementation', suffix: 'control-implementation' }, - { label: 'Export Offerings', suffix: 'export-offerings' }, - { label: 'Leverage', suffix: 'leverage' }, - { label: 'Inherited Capabilities', suffix: 'inherited-capabilities' }, + // { label: 'Leveraged Authorizations', suffix: 'authorizations' }, + // { label: 'Control Implementation', suffix: 'control-implementation' }, + // { label: 'Export Offerings', suffix: 'export-offerings' }, + // { label: 'Leverage', suffix: 'leverage' }, + // { label: 'Inherited Capabilities', suffix: 'inherited-capabilities' }, { label: 'Risks', suffix: 'risks' }, { label: 'Compliance', suffix: 'compliance' }, { label: 'JSON', suffix: 'json' }, diff --git a/src/views/LeftSideNav.vue b/src/views/LeftSideNav.vue index 56e2850b..f17ea600 100644 --- a/src/views/LeftSideNav.vue +++ b/src/views/LeftSideNav.vue @@ -43,15 +43,81 @@ function abbreviated(link: NavigationItem): string { const links = ref>([ { - name: 'system:overview', - title: 'Active System', - abbr: 'SYS', + title: 'Systems', + children: [ + { + name: 'system:overview', + title: 'Active System', + abbr: 'SYS', + }, + { + name: 'system-security-plans', + title: 'Manage Systems', + permission: { resource: RESOURCES.SSP, action: ACTIONS.READ }, + }, + ], + }, + { + name: 'evidence:index', + title: 'Evidence', + abbr: 'EV', + permission: { resource: RESOURCES.EVIDENCE, action: ACTIONS.READ }, }, { name: 'controls:index', title: 'Controls', abbr: 'CON', }, + { + name: 'lineage', + title: 'Compliance Map', + abbr: 'CM', + }, + { + name: 'risks:index', + title: 'Risk Register', + abbr: 'RIS', + permission: { resource: RESOURCES.RISK, action: ACTIONS.READ }, + }, + { + name: 'dashboards', + title: 'Evidence Filters', + abbr: 'FIL', + permission: { resource: RESOURCES.FILTER, action: ACTIONS.READ }, + }, + // { + // name: 'inventory:index', + // title: 'Inventory', + // abbr: 'INV', + // permission: { resource: RESOURCES.INVENTORY, action: ACTIONS.READ }, + // }, + // { + // title: 'Implementation', + // children: [ + // { + // name: 'assessment-plans', + // title: 'Assessment Plans', + // permission: { + // resource: RESOURCES.ASSESSMENT_PLAN, + // action: ACTIONS.READ, + // }, + // }, + // { + // name: 'assessment-results', + // title: 'Assessment Results', + // permission: { + // resource: RESOURCES.ASSESSMENT_RESULTS, + // action: ACTIONS.READ, + // }, + // }, + // { + // name: 'plan-of-action-and-milestones', + // title: 'POA&M', + // abbr: 'PM', + // permission: { resource: RESOURCES.POAM_OSCAL, action: ACTIONS.READ }, + // }, + // ], + // }, { title: 'Workflows', abbr: 'WF', @@ -77,32 +143,9 @@ const links = ref>([ ], }, { - name: 'risks:index', - title: 'Risk Register', - abbr: 'RIS', - permission: { resource: RESOURCES.RISK, action: ACTIONS.READ }, - }, - { - name: 'inventory:index', - title: 'Inventory', - abbr: 'INV', - permission: { resource: RESOURCES.INVENTORY, action: ACTIONS.READ }, - }, - { - name: 'evidence:index', - title: 'Evidence', - abbr: 'EV', - permission: { resource: RESOURCES.EVIDENCE, action: ACTIONS.READ }, - }, - { - title: 'Governance', - abbr: 'GOV', + title: 'Control Definitions', + abbr: 'CD', children: [ - { - name: 'admin-risks', - title: 'Risks', - permission: ADMIN_MANAGE, - }, { name: 'catalog-list', title: 'Catalogs', @@ -121,72 +164,34 @@ const links = ref>([ abbr: 'PR', permission: { resource: RESOURCES.PROFILE, action: ACTIONS.READ }, }, - { - name: 'lineage', - title: 'Compliance Map', - abbr: 'CM', - }, - ], - }, - { - title: 'Implementation', - children: [ - { - name: 'system-security-plans', - title: 'System Security Plans', - permission: { resource: RESOURCES.SSP, action: ACTIONS.READ }, - }, - { - name: 'assessment-plans', - title: 'Assessment Plans', - permission: { - resource: RESOURCES.ASSESSMENT_PLAN, - action: ACTIONS.READ, - }, - }, - { - name: 'assessment-results', - title: 'Assessment Results', - permission: { - resource: RESOURCES.ASSESSMENT_RESULTS, - action: ACTIONS.READ, - }, - }, - { - name: 'plan-of-action-and-milestones', - title: 'POA&M', - abbr: 'PM', - permission: { resource: RESOURCES.POAM_OSCAL, action: ACTIONS.READ }, - }, ], }, { title: 'Admin', children: [ { - name: 'dashboards', - title: 'Filters', - abbr: 'FIL', - permission: { resource: RESOURCES.FILTER, action: ACTIONS.READ }, - }, - { - name: 'component-definitions', - title: 'Component Definitions', - permission: { - resource: RESOURCES.COMPONENT_DEFINITION, - action: ACTIONS.READ, - }, - }, - { - name: 'admin-parties', - title: 'Parties', - permission: { resource: RESOURCES.PARTY, action: ACTIONS.READ }, - }, - { - name: 'admin-roles', - title: 'Roles', - permission: { resource: RESOURCES.ROLE, action: ACTIONS.READ }, + name: 'admin-risks', + title: 'Risks', + permission: ADMIN_MANAGE, }, + // { + // name: 'component-definitions', + // title: 'Component Definitions', + // permission: { + // resource: RESOURCES.COMPONENT_DEFINITION, + // action: ACTIONS.READ, + // }, + // }, + // { + // name: 'admin-parties', + // title: 'Parties', + // permission: { resource: RESOURCES.PARTY, action: ACTIONS.READ }, + // }, + // { + // name: 'admin-roles', + // title: 'Roles', + // permission: { resource: RESOURCES.ROLE, action: ACTIONS.READ }, + // }, { name: 'users-list', title: 'System Users', @@ -202,16 +207,16 @@ const links = ref>([ title: 'Agents', permission: ADMIN_MANAGE, }, - { - name: 'admin-subject-templates', - title: 'Subject Templates', - permission: ADMIN_MANAGE, - }, - { - name: 'admin-risk-templates', - title: 'Risk Templates', - permission: ADMIN_MANAGE, - }, + // { + // name: 'admin-subject-templates', + // title: 'Subject Templates', + // permission: ADMIN_MANAGE, + // }, + // { + // name: 'admin-risk-templates', + // title: 'Risk Templates', + // permission: ADMIN_MANAGE, + // }, { name: 'admin-diagnostics', title: 'Diagnostics', diff --git a/src/views/__tests__/LeftSideNav.spec.ts b/src/views/__tests__/LeftSideNav.spec.ts index d37bcbec..e5179197 100644 --- a/src/views/__tests__/LeftSideNav.spec.ts +++ b/src/views/__tests__/LeftSideNav.spec.ts @@ -9,7 +9,7 @@ describe('LeftSideNav', () => { setActivePinia(createPinia()); }); - it('renders admin links in correct order', () => { + it('no longer renders the retired admin links', () => { const sidebarStore = useSidebarStore(); sidebarStore.open = true; @@ -43,32 +43,57 @@ describe('LeftSideNav', () => { .map((link) => link.text().trim()) .filter((text) => text.length > 0); - const systemUsersIndex = linkTexts.indexOf('System Users'); - const agentsIndex = linkTexts.indexOf('Agents'); - const diagnosticsIndex = linkTexts.indexOf('Diagnostics'); - const subjectTemplatesIndex = linkTexts.indexOf('Subject Templates'); - const riskTemplatesIndex = linkTexts.indexOf('Risk Templates'); - const importIndex = linkTexts.indexOf('Import'); - - for (const index of [ - systemUsersIndex, - agentsIndex, - diagnosticsIndex, - subjectTemplatesIndex, - riskTemplatesIndex, - importIndex, + for (const label of [ + 'Subject Templates', + 'Risk Templates', + 'Component Definitions', + 'Parties', + 'Roles', ]) { - expect(index).toBeGreaterThanOrEqual(0); + expect(linkTexts).not.toContain(label); } + }); + + it('reinstates Groups, Agents, Diagnostics and Import in the navigation', () => { + const sidebarStore = useSidebarStore(); + sidebarStore.open = true; + + const wrapper = mount(LeftSideNav, { + global: { + directives: { + tooltip: { + mounted: () => undefined, + }, + }, + stubs: { + SideNav: { + template: '
', + }, + SideNavCategory: { + template: + '
', + }, + SideNavLink: { + template: '', + }, + SideNavLogo: { + template: 'logo', + }, + }, + }, + }); + + const linkTexts = wrapper + .findAll('.sidenav-link') + .map((link) => link.text().trim()) + .filter((text) => text.length > 0); - expect(agentsIndex).toBeGreaterThan(systemUsersIndex); - expect(subjectTemplatesIndex).toBeGreaterThan(agentsIndex); - expect(riskTemplatesIndex).toBeGreaterThan(subjectTemplatesIndex); - expect(diagnosticsIndex).toBeGreaterThan(riskTemplatesIndex); - expect(importIndex).toBeGreaterThan(diagnosticsIndex); + for (const label of ['Groups', 'Agents', 'Diagnostics', 'Import']) { + expect(linkTexts).toContain(label); + } }); - it('groups Risks, Catalogs, Control Links, Profiles and Compliance Map under Governance, with Filters under Admin', () => { + it('groups Risks, Groups, Agents, Diagnostics and Import under a separate Admin category', () => { const sidebarStore = useSidebarStore(); sidebarStore.open = true; @@ -98,38 +123,85 @@ describe('LeftSideNav', () => { }); const categories = wrapper.findAll('section'); - const governanceCategory = categories.find( - (category) => category.find('.category-title').text() === 'Governance', - ); const adminCategory = categories.find( (category) => category.find('.category-title').text() === 'Admin', ); - expect(governanceCategory).toBeTruthy(); + expect(adminCategory).toBeTruthy(); for (const label of [ 'Risks', - 'Catalogs', - 'Control Links', - 'Profiles', - 'Compliance Map', + 'Groups', + 'Agents', + 'Diagnostics', + 'Import', ]) { - expect(governanceCategory?.text()).toContain(label); - expect(adminCategory?.text()).not.toContain(label); + expect(adminCategory?.text()).toContain(label); } - // Filters live under Admin, not Governance. - expect(adminCategory?.text()).toContain('Filters'); - expect(governanceCategory?.text()).not.toContain('Filters'); + // Not nested under Admin — Control Definitions still owns these. + expect(adminCategory?.text()).not.toContain('Catalogs'); + expect(adminCategory?.text()).not.toContain('Control Links'); + }); + + it('groups Catalogs, Control Links and Profiles under Control Definitions, with Evidence Filters and Compliance Map as top-level links', () => { + const sidebarStore = useSidebarStore(); + sidebarStore.open = true; + + const wrapper = mount(LeftSideNav, { + global: { + directives: { + tooltip: { + mounted: () => undefined, + }, + }, + stubs: { + SideNav: { + template: '
', + }, + SideNavCategory: { + template: + '
', + }, + SideNavLink: { + template: '', + }, + SideNavLogo: { + template: 'logo', + }, + }, + }, + }); + + const categories = wrapper.findAll('section'); + const controlDefinitionsCategory = categories.find( + (category) => + category.find('.category-title').text() === 'Control Definitions', + ); + + expect(controlDefinitionsCategory).toBeTruthy(); + + for (const label of ['Catalogs', 'Control Links', 'Profiles']) { + expect(controlDefinitionsCategory?.text()).toContain(label); + } + expect(controlDefinitionsCategory?.text()).not.toContain('Risks'); + expect(controlDefinitionsCategory?.text()).not.toContain( + 'Evidence Filters', + ); + expect(controlDefinitionsCategory?.text()).not.toContain('Compliance Map'); - // Dashboards and Lineage are no longer standalone top-level links. + // Evidence Filters and Compliance Map are now standalone top-level links, + // not nested under any category. const topLevelLinks = wrapper .findAll('.sidenav-link') .map((link) => link.text().trim()); + expect(topLevelLinks).toContain('Evidence Filters'); + expect(topLevelLinks).toContain('Compliance Map'); + + // Dashboards is still not a standalone top-level link. expect(topLevelLinks).not.toContain('Dashboards'); - expect(topLevelLinks).not.toContain('Lineage'); }); - it('exposes import only from the admin navigation category', () => { + it('groups Active System and Manage Systems under a Systems category', () => { const sidebarStore = useSidebarStore(); sidebarStore.open = true; @@ -159,17 +231,71 @@ describe('LeftSideNav', () => { }); const categories = wrapper.findAll('section'); - const adminCategory = categories.find( - (category) => category.find('.category-title').text() === 'Admin', + const systemsCategory = categories.find( + (category) => category.find('.category-title').text() === 'Systems', ); - expect(adminCategory?.text()).toContain('Import'); - expect( - categories - .filter( - (category) => category.find('.category-title').text() !== 'Admin', - ) - .some((category) => category.text().includes('Import')), - ).toBe(false); + expect(systemsCategory).toBeTruthy(); + expect(systemsCategory?.text()).toContain('Active System'); + expect(systemsCategory?.text()).toContain('Manage Systems'); + expect(systemsCategory?.text()).not.toContain('System Security Plans'); + }); + + it('puts Systems at the top, followed by Evidence, Controls, Compliance Map, Risk Register and Evidence Filters', () => { + const sidebarStore = useSidebarStore(); + sidebarStore.open = true; + + const wrapper = mount(LeftSideNav, { + global: { + directives: { + tooltip: { + mounted: () => undefined, + }, + }, + stubs: { + SideNav: { + template: '
', + }, + SideNavCategory: { + template: + '
', + }, + SideNavLink: { + template: '', + }, + SideNavLogo: { + template: 'logo', + }, + }, + }, + }); + + const orderedLabels = wrapper + .findAll('.category-title, .sidenav-link') + .map((el) => el.text().trim()); + const categoryTitles = wrapper + .findAll('.category-title') + .map((el) => el.text().trim()); + + const indexOf = (label: string) => orderedLabels.indexOf(label); + + // Systems is the first top-level category, with its children rendered + // immediately after it, followed by Evidence, Controls, Compliance Map, + // Risk Register and Evidence Filters, in that order. + expect(categoryTitles.at(0)).toBe('Systems'); + expect(orderedLabels.slice(0, 8)).toEqual([ + 'Systems', + 'Active System', + 'Manage Systems', + 'Evidence', + 'Controls', + 'Compliance Map', + 'Risk Register', + 'Evidence Filters', + ]); + + for (const label of ['Workflows', 'Control Definitions']) { + expect(indexOf('Systems')).toBeLessThan(indexOf(label)); + } }); }); diff --git a/src/views/__tests__/NotificationsView.spec.ts b/src/views/__tests__/NotificationsView.spec.ts index 17972d5e..6d44a50d 100644 --- a/src/views/__tests__/NotificationsView.spec.ts +++ b/src/views/__tests__/NotificationsView.spec.ts @@ -1286,7 +1286,7 @@ describe('NotificationsView', () => { expect(wrapper.text()).toContain('ccf-alerts'); }); - it('renders diagnostics checks for digest, workflow, risk, and POAM notifications', async () => { + it('renders diagnostics checks for digest, workflow, and risk notifications', async () => { const wrapper = mount(NotificationsView); await flushPromises(); @@ -1317,10 +1317,25 @@ describe('NotificationsView', () => { await flushPromises(); expect(wrapper.text()).toContain('Risk review due reminder'); expect(wrapper.text()).toContain('Warn'); + }); + + it('does not offer POAM Notifications as a diagnostics option', async () => { + const wrapper = mount(NotificationsView); + await flushPromises(); - await diagnosticsSelect.setValue('POAM_NOTIFICATIONS'); + await wrapper + .findAll('button') + .find((button) => button.text() === 'Diagnostics')! + .trigger('click'); await flushPromises(); - expect(wrapper.text()).toContain('POAM deadline reminder'); + + const diagnosticsSelect = wrapper + .find('#notifications-diagnostics-panel') + .find('select'); + const optionLabels = diagnosticsSelect + .findAll('option') + .map((option) => option.text()); + expect(optionLabels).not.toContain('POAM Notifications'); }); it('links diagnostics results to filtered deliveries', async () => { diff --git a/src/views/admin/NotificationsView.vue b/src/views/admin/NotificationsView.vue index c091df4f..5958c490 100644 --- a/src/views/admin/NotificationsView.vue +++ b/src/views/admin/NotificationsView.vue @@ -251,7 +251,7 @@ const diagnosticsNotificationOptions: NotificationOption[] = [ { label: 'Evidence Digest', value: 'EVIDENCE_DIGEST' }, { label: 'Workflow Notifications', value: 'WORKFLOW_EXECUTION_FAILED' }, { label: 'Risk Notifications', value: 'RISK_NOTIFICATIONS' }, - { label: 'POAM Notifications', value: 'POAM_NOTIFICATIONS' }, + // { label: 'POAM Notifications', value: 'POAM_NOTIFICATIONS' }, ]; const timeRangeOptions: NotificationOption[] = [ { label: 'Last 24 hours', value: '24h' }, diff --git a/src/views/dashboard/IndexView.vue b/src/views/dashboard/IndexView.vue index 3f2285ad..21272942 100644 --- a/src/views/dashboard/IndexView.vue +++ b/src/views/dashboard/IndexView.vue @@ -1,7 +1,7 @@