Fix navigation structure, verbose descriptions, and naming mismatches
Navigation: - Move Diagnostics and Notifications from Settings to Operations sidebar group (routes are /ops/operations/*) - Policy: skip redundant Overview tab, land directly on Packs (the first actionable tab) - Policy: remove "Ops / Policy" eyebrow prefix (breadcrumb already shows this) Naming: - Audit Log: "Unified Audit Log" → "Audit Log" to match sidebar label - Evidence: "Evidence & Audit" → "Evidence Overview" to match sidebar label Verbose descriptions trimmed: - Policy shell: single-line subtitle, remove default contextNote - Evidence overview: remove second paragraph about Operator/Auditor modes - Operations hub: trim to "Platform health, execution control, diagnostics, and airgap workflows." - Deployments: trim to "Deployment runs, approvals, and promotion activity." - Integrations Hub tab: remove duplicate heading (parent shell already provides it) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -41,7 +41,7 @@ const AUDIT_TABS: StellaPageTab[] = [
|
|||||||
template: `
|
template: `
|
||||||
<div class="audit-dashboard">
|
<div class="audit-dashboard">
|
||||||
<header class="page-header">
|
<header class="page-header">
|
||||||
<h1>Unified Audit Log</h1>
|
<h1>Audit Log</h1>
|
||||||
<p class="description">Cross-module audit trail visibility for compliance and governance</p>
|
<p class="description">Cross-module audit trail visibility for compliance and governance</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -38,12 +38,9 @@ type EvidenceHomeMode = 'normal' | 'degraded' | 'empty';
|
|||||||
<div class="evidence-audit-overview">
|
<div class="evidence-audit-overview">
|
||||||
<header class="overview-header">
|
<header class="overview-header">
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
<h1 class="overview-title">Evidence & Audit</h1>
|
<h1 class="overview-title">Evidence Overview</h1>
|
||||||
<p class="overview-subtitle">
|
<p class="overview-subtitle">
|
||||||
Retrieve, verify, export, and audit evidence for every release, bundle, environment, and approval decision.
|
Retrieve, verify, export, and audit evidence for every release decision.
|
||||||
</p>
|
|
||||||
<p class="overview-hint">
|
|
||||||
Operator mode keeps the action path concise. Auditor mode expands provenance and proof detail for formal review.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Component, inject, OnDestroy, signal } from '@angular/core';
|
|||||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||||
|
|
||||||
import { PageActionService } from '../../core/services/page-action.service';
|
import { PageActionService } from '../../core/services/page-action.service';
|
||||||
|
import { PageActionOutletComponent } from '../../shared/components/page-action-outlet/page-action-outlet.component';
|
||||||
import { IntegrationService } from './integration.service';
|
import { IntegrationService } from './integration.service';
|
||||||
import { IntegrationType } from './integration.models';
|
import { IntegrationType } from './integration.models';
|
||||||
|
|
||||||
@@ -18,19 +19,16 @@ interface IntegrationHubStats {
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-integration-hub',
|
selector: 'app-integration-hub',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [RouterModule],
|
imports: [RouterModule, PageActionOutletComponent],
|
||||||
template: `
|
template: `
|
||||||
<section class="integration-hub">
|
<section class="integration-hub">
|
||||||
<header class="hub-header">
|
<header class="hub-header">
|
||||||
<div>
|
<div class="hub-header__right">
|
||||||
<h1>Integrations</h1>
|
<app-page-action-outlet />
|
||||||
<p class="hub-subtitle">
|
<div class="hub-summary" aria-live="polite">
|
||||||
Connect the external systems Stella Ops depends on, then verify them from the same setup surface.
|
<strong>{{ configuredConnectorCount() }}</strong>
|
||||||
</p>
|
<span>configured connectors</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hub-summary" aria-live="polite">
|
|
||||||
<strong>{{ configuredConnectorCount() }}</strong>
|
|
||||||
<span>configured connectors</span>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -147,6 +145,12 @@ interface IntegrationHubStats {
|
|||||||
max-width: 58ch;
|
max-width: 58ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hub-header__right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.hub-summary {
|
.hub-summary {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.1rem;
|
gap: 0.1rem;
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
<div class="ops-overview__title-group">
|
<div class="ops-overview__title-group">
|
||||||
<h1>Operations</h1>
|
<h1>Operations</h1>
|
||||||
<p class="ops-overview__subtitle">
|
<p class="ops-overview__subtitle">
|
||||||
Consolidated operator shell for blocking platform issues, execution control, diagnostics,
|
Platform health, execution control, diagnostics, and airgap workflows.
|
||||||
and airgap workflows. Topology and agent ownership remain under Setup.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import {
|
|||||||
import { StellaPageTabsComponent, StellaPageTab } from '../../shared/components/stella-page-tabs/stella-page-tabs.component';
|
import { StellaPageTabsComponent, StellaPageTab } from '../../shared/components/stella-page-tabs/stella-page-tabs.component';
|
||||||
|
|
||||||
type DecisioningPrimaryTab =
|
type DecisioningPrimaryTab =
|
||||||
| 'overview'
|
|
||||||
| 'packs'
|
| 'packs'
|
||||||
| 'governance'
|
| 'governance'
|
||||||
| 'simulation'
|
| 'simulation'
|
||||||
@@ -57,7 +56,6 @@ interface DecisioningShellState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PAGE_TABS: readonly StellaPageTab[] = [
|
const PAGE_TABS: readonly StellaPageTab[] = [
|
||||||
{ id: 'overview', label: 'Overview', icon: 'M3 3h7v7H3z|||M14 3h7v7h-7z|||M14 14h7v7h-7z|||M3 14h7v7H3z' },
|
|
||||||
{ id: 'packs', label: 'Packs', icon: 'M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z|||M3.27 6.96L12 12.01l8.73-5.05|||M12 22.08V12' },
|
{ id: 'packs', label: 'Packs', icon: 'M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z|||M3.27 6.96L12 12.01l8.73-5.05|||M12 22.08V12' },
|
||||||
{ id: 'governance', label: 'Governance', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' },
|
{ id: 'governance', label: 'Governance', icon: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' },
|
||||||
{ id: 'simulation', label: 'Simulation', icon: 'M5 3l14 9-14 9V3z' },
|
{ id: 'simulation', label: 'Simulation', icon: 'M5 3l14 9-14 9V3z' },
|
||||||
@@ -78,7 +76,7 @@ const PAGE_TABS: readonly StellaPageTab[] = [
|
|||||||
template: `
|
template: `
|
||||||
<section class="policy-decisioning-shell" data-testid="policy-decisioning-shell">
|
<section class="policy-decisioning-shell" data-testid="policy-decisioning-shell">
|
||||||
<app-context-header
|
<app-context-header
|
||||||
eyebrow="Ops / Policy"
|
[eyebrow]="null"
|
||||||
[title]="headerTitle()"
|
[title]="headerTitle()"
|
||||||
[subtitle]="headerSubtitle()"
|
[subtitle]="headerSubtitle()"
|
||||||
[contextNote]="headerNote()"
|
[contextNote]="headerNote()"
|
||||||
@@ -99,6 +97,7 @@ const PAGE_TABS: readonly StellaPageTab[] = [
|
|||||||
<stella-page-tabs
|
<stella-page-tabs
|
||||||
[tabs]="pageTabs"
|
[tabs]="pageTabs"
|
||||||
[activeTab]="shellState().activeTab"
|
[activeTab]="shellState().activeTab"
|
||||||
|
urlParam="tab"
|
||||||
ariaLabel="Policy decisioning tabs"
|
ariaLabel="Policy decisioning tabs"
|
||||||
(tabChange)="onTabChange($event)"
|
(tabChange)="onTabChange($event)"
|
||||||
>
|
>
|
||||||
@@ -173,7 +172,7 @@ export class PolicyDecisioningShellComponent {
|
|||||||
case 'evidence':
|
case 'evidence':
|
||||||
return 'Trace evidence, gate posture, and policy or VEX actions from a single canonical route.';
|
return 'Trace evidence, gate posture, and policy or VEX actions from a single canonical route.';
|
||||||
default:
|
default:
|
||||||
return 'One canonical shell for policy packs, governance, simulation, VEX, exceptions, release gates, and audit.';
|
return 'Policy packs, governance, simulation, VEX, exceptions, release gates, and audit.';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -192,7 +191,7 @@ export class PolicyDecisioningShellComponent {
|
|||||||
case 'evidence':
|
case 'evidence':
|
||||||
return 'Evidence context is non-owning: Decisioning Studio stays focused on gates, policy, and VEX actions.';
|
return 'Evidence context is non-owning: Decisioning Studio stays focused on gates, policy, and VEX actions.';
|
||||||
default:
|
default:
|
||||||
return 'Use the primary tabs to move between policy packs, governance, simulation, release gates, VEX, and audit.';
|
return null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -420,7 +419,7 @@ function resolvePrimaryTab(currentUrl: string): DecisioningPrimaryTab {
|
|||||||
return 'audit';
|
return 'audit';
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'overview';
|
return 'packs';
|
||||||
}
|
}
|
||||||
|
|
||||||
function coerceString(value: unknown): string | null {
|
function coerceString(value: unknown): string | null {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const policyDecisioningRoutes: Routes = [
|
|||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
pathMatch: 'full',
|
pathMatch: 'full',
|
||||||
redirectTo: 'overview',
|
redirectTo: 'packs',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'overview',
|
path: 'overview',
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ function deriveOutcomeIcon(status: string): string {
|
|||||||
<section class="activity">
|
<section class="activity">
|
||||||
<header>
|
<header>
|
||||||
<h1>Deployments</h1>
|
<h1>Deployments</h1>
|
||||||
<p>Deployment activity across timeline/table/correlations with lane and operability filtering.</p>
|
<p>Deployment runs, approvals, and promotion activity.</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="context">
|
<div class="context">
|
||||||
|
|||||||
@@ -107,23 +107,6 @@ interface NavSectionGroup {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Collapse toggle - circle on the sidebar/content border -->
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="sidebar__collapse-btn-edge"
|
|
||||||
(click)="collapseToggle.emit()"
|
|
||||||
[attr.title]="collapsed ? 'Expand sidebar' : 'Collapse sidebar'"
|
|
||||||
[attr.aria-label]="collapsed ? 'Expand sidebar' : 'Collapse sidebar'"
|
|
||||||
>
|
|
||||||
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true">
|
|
||||||
@if (collapsed) {
|
|
||||||
<polyline points="9 18 15 12 9 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
||||||
} @else {
|
|
||||||
<polyline points="15 18 9 12 15 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
||||||
}
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Navigation - collapsible groups with foldable sections -->
|
<!-- Navigation - collapsible groups with foldable sections -->
|
||||||
<nav class="sidebar__nav" #sidebarNav>
|
<nav class="sidebar__nav" #sidebarNav>
|
||||||
@for (group of displaySectionGroups(); track group.id) {
|
@for (group of displaySectionGroups(); track group.id) {
|
||||||
@@ -243,7 +226,7 @@ interface NavSectionGroup {
|
|||||||
-webkit-backdrop-filter: blur(16px);
|
-webkit-backdrop-filter: blur(16px);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
4px 0 24px rgba(0, 0, 0, 0.3),
|
4px 0 24px rgba(0, 0, 0, 0.3),
|
||||||
1px 0 0 rgba(245, 166, 35, 0.08);
|
1px 0 0 var(--color-brand-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar--collapsed.sidebar--flyout .sb-group__header,
|
.sidebar--collapsed.sidebar--flyout .sb-group__header,
|
||||||
@@ -267,7 +250,7 @@ interface NavSectionGroup {
|
|||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
180deg,
|
180deg,
|
||||||
var(--color-sidebar-border) 0%,
|
var(--color-sidebar-border) 0%,
|
||||||
rgba(245, 166, 35, 0.08) 50%,
|
var(--color-brand-soft) 50%,
|
||||||
var(--color-sidebar-border) 100%
|
var(--color-sidebar-border) 100%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -376,46 +359,7 @@ interface NavSectionGroup {
|
|||||||
/* ================================================================
|
/* ================================================================
|
||||||
Collapse button - circle on sidebar/content border
|
Collapse button - circle on sidebar/content border
|
||||||
================================================================ */
|
================================================================ */
|
||||||
.sidebar__collapse-btn-edge {
|
/* Collapse toggle button is now rendered by AppShellComponent to avoid overflow:hidden clipping */
|
||||||
position: absolute;
|
|
||||||
top: 28px;
|
|
||||||
right: -12px;
|
|
||||||
z-index: 10;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 1px solid var(--color-sidebar-border);
|
|
||||||
background: var(--color-sidebar-bg);
|
|
||||||
color: var(--color-sidebar-text-muted);
|
|
||||||
cursor: pointer;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.2s, color 0.2s, background 0.2s, border-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar:hover .sidebar__collapse-btn-edge,
|
|
||||||
.sidebar__collapse-btn-edge:focus-visible {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar__collapse-btn-edge:hover {
|
|
||||||
color: var(--color-sidebar-active-text);
|
|
||||||
background: rgba(245, 166, 35, 0.12);
|
|
||||||
border-color: rgba(245, 166, 35, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar__collapse-btn-edge:focus-visible {
|
|
||||||
outline: 1.5px solid var(--color-sidebar-active-border);
|
|
||||||
outline-offset: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 991px) {
|
|
||||||
.sidebar__collapse-btn-edge {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ================================================================
|
/* ================================================================
|
||||||
Scrollable nav area
|
Scrollable nav area
|
||||||
@@ -472,7 +416,7 @@ interface NavSectionGroup {
|
|||||||
90deg,
|
90deg,
|
||||||
transparent 0%,
|
transparent 0%,
|
||||||
var(--color-sidebar-divider) 20%,
|
var(--color-sidebar-divider) 20%,
|
||||||
rgba(245, 166, 35, 0.06) 50%,
|
var(--color-sidebar-hover) 50%,
|
||||||
var(--color-sidebar-divider) 80%,
|
var(--color-sidebar-divider) 80%,
|
||||||
transparent 100%
|
transparent 100%
|
||||||
);
|
);
|
||||||
@@ -499,7 +443,7 @@ interface NavSectionGroup {
|
|||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--color-sidebar-active-text);
|
color: var(--color-sidebar-active-text);
|
||||||
background: rgba(245, 166, 35, 0.04);
|
background: var(--color-sidebar-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
@@ -513,7 +457,7 @@ interface NavSectionGroup {
|
|||||||
width: 4px;
|
width: 4px;
|
||||||
height: 4px;
|
height: 4px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: rgba(245, 166, 35, 0.4);
|
background: var(--color-border-emphasis);
|
||||||
transition: background 0.2s, transform 0.2s;
|
transition: background 0.2s, transform 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,7 +467,7 @@ interface NavSectionGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sb-group--collapsed .sb-group__dot {
|
.sb-group--collapsed .sb-group__dot {
|
||||||
background: rgba(245, 166, 35, 0.2);
|
background: var(--color-brand-primary-20);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sb-group__title {
|
.sb-group__title {
|
||||||
@@ -602,8 +546,8 @@ interface NavSectionGroup {
|
|||||||
width: 1px;
|
width: 1px;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
180deg,
|
180deg,
|
||||||
rgba(245, 166, 35, 0.2) 0%,
|
var(--color-brand-primary-20) 0%,
|
||||||
rgba(245, 166, 35, 0.08) 100%
|
var(--color-brand-soft) 100%
|
||||||
);
|
);
|
||||||
border-radius: 1px;
|
border-radius: 1px;
|
||||||
}
|
}
|
||||||
@@ -660,6 +604,15 @@ export class AppSidebarComponent implements AfterViewInit {
|
|||||||
|
|
||||||
@Output() mobileClose = new EventEmitter<void>();
|
@Output() mobileClose = new EventEmitter<void>();
|
||||||
@Output() collapseToggle = new EventEmitter<void>();
|
@Output() collapseToggle = new EventEmitter<void>();
|
||||||
|
|
||||||
|
/** Handle collapse button click — toggles sidebar on desktop, toggles mobile menu on mobile. */
|
||||||
|
onCollapseClick(): void {
|
||||||
|
if (window.innerWidth <= 991) {
|
||||||
|
this.mobileClose.emit();
|
||||||
|
} else {
|
||||||
|
this.collapseToggle.emit();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ViewChild('sidebarNav', { static: false }) sidebarNavRef!: ElementRef<HTMLElement>;
|
@ViewChild('sidebarNav', { static: false }) sidebarNavRef!: ElementRef<HTMLElement>;
|
||||||
|
|
||||||
private flyoutEnterTimer: ReturnType<typeof setTimeout> | null = null;
|
private flyoutEnterTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
@@ -724,7 +677,7 @@ export class AppSidebarComponent implements AfterViewInit {
|
|||||||
{
|
{
|
||||||
id: 'vulnerabilities',
|
id: 'vulnerabilities',
|
||||||
label: 'Vulnerabilities',
|
label: 'Vulnerabilities',
|
||||||
icon: 'list',
|
icon: 'alert',
|
||||||
route: '/triage/artifacts',
|
route: '/triage/artifacts',
|
||||||
menuGroupId: 'security',
|
menuGroupId: 'security',
|
||||||
menuGroupLabel: 'Security',
|
menuGroupLabel: 'Security',
|
||||||
@@ -800,7 +753,7 @@ export class AppSidebarComponent implements AfterViewInit {
|
|||||||
{
|
{
|
||||||
id: 'ops-jobs',
|
id: 'ops-jobs',
|
||||||
label: 'Scheduled Jobs',
|
label: 'Scheduled Jobs',
|
||||||
icon: 'clock',
|
icon: 'calendar',
|
||||||
route: '/ops/operations/jobengine',
|
route: '/ops/operations/jobengine',
|
||||||
menuGroupId: 'operations',
|
menuGroupId: 'operations',
|
||||||
menuGroupLabel: 'Operations',
|
menuGroupLabel: 'Operations',
|
||||||
@@ -809,6 +762,18 @@ export class AppSidebarComponent implements AfterViewInit {
|
|||||||
StellaOpsScopes.ORCH_OPERATE,
|
StellaOpsScopes.ORCH_OPERATE,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'ops-scripts',
|
||||||
|
label: 'Scripts',
|
||||||
|
icon: 'code',
|
||||||
|
route: '/ops/scripts',
|
||||||
|
menuGroupId: 'operations',
|
||||||
|
menuGroupLabel: 'Operations',
|
||||||
|
requireAnyScope: [
|
||||||
|
StellaOpsScopes.ORCH_READ,
|
||||||
|
StellaOpsScopes.ORCH_OPERATE,
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'ops-signals',
|
id: 'ops-signals',
|
||||||
label: 'Signals',
|
label: 'Signals',
|
||||||
@@ -836,7 +801,7 @@ export class AppSidebarComponent implements AfterViewInit {
|
|||||||
{
|
{
|
||||||
id: 'ops-policy',
|
id: 'ops-policy',
|
||||||
label: 'Policy',
|
label: 'Policy',
|
||||||
icon: 'shield',
|
icon: 'clipboard',
|
||||||
route: '/ops/policy',
|
route: '/ops/policy',
|
||||||
menuGroupId: 'operations',
|
menuGroupId: 'operations',
|
||||||
menuGroupLabel: 'Operations',
|
menuGroupLabel: 'Operations',
|
||||||
@@ -847,8 +812,8 @@ export class AppSidebarComponent implements AfterViewInit {
|
|||||||
label: 'Diagnostics',
|
label: 'Diagnostics',
|
||||||
icon: 'stethoscope',
|
icon: 'stethoscope',
|
||||||
route: '/ops/operations/doctor',
|
route: '/ops/operations/doctor',
|
||||||
menuGroupId: 'setup-admin',
|
menuGroupId: 'operations',
|
||||||
menuGroupLabel: 'Settings',
|
menuGroupLabel: 'Operations',
|
||||||
requireAnyScope: [StellaOpsScopes.HEALTH_READ, StellaOpsScopes.UI_ADMIN],
|
requireAnyScope: [StellaOpsScopes.HEALTH_READ, StellaOpsScopes.UI_ADMIN],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -856,8 +821,8 @@ export class AppSidebarComponent implements AfterViewInit {
|
|||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
icon: 'bell',
|
icon: 'bell',
|
||||||
route: '/ops/operations/notifications',
|
route: '/ops/operations/notifications',
|
||||||
menuGroupId: 'setup-admin',
|
menuGroupId: 'operations',
|
||||||
menuGroupLabel: 'Settings',
|
menuGroupLabel: 'Operations',
|
||||||
requireAnyScope: [StellaOpsScopes.NOTIFY_VIEWER],
|
requireAnyScope: [StellaOpsScopes.NOTIFY_VIEWER],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -872,6 +837,24 @@ export class AppSidebarComponent implements AfterViewInit {
|
|||||||
StellaOpsScopes.VEX_READ,
|
StellaOpsScopes.VEX_READ,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'ops-watchlist',
|
||||||
|
label: 'Watchlist',
|
||||||
|
icon: 'eye',
|
||||||
|
route: '/ops/operations/watchlist',
|
||||||
|
menuGroupId: 'operations',
|
||||||
|
menuGroupLabel: 'Operations',
|
||||||
|
requireAnyScope: [StellaOpsScopes.SIGNER_READ],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ops-trust-analytics',
|
||||||
|
label: 'Trust Analytics',
|
||||||
|
icon: 'trending-up',
|
||||||
|
route: '/ops/operations/trust-analytics',
|
||||||
|
menuGroupId: 'operations',
|
||||||
|
menuGroupLabel: 'Operations',
|
||||||
|
requireAnyScope: [StellaOpsScopes.SIGNER_READ],
|
||||||
|
},
|
||||||
// ── Group 4: Audit & Evidence ────────────────────────────────────
|
// ── Group 4: Audit & Evidence ────────────────────────────────────
|
||||||
{
|
{
|
||||||
id: 'evidence-overview',
|
id: 'evidence-overview',
|
||||||
@@ -927,7 +910,7 @@ export class AppSidebarComponent implements AfterViewInit {
|
|||||||
{
|
{
|
||||||
id: 'evidence-audit-log',
|
id: 'evidence-audit-log',
|
||||||
label: 'Audit Log',
|
label: 'Audit Log',
|
||||||
icon: 'book-open',
|
icon: 'list',
|
||||||
route: '/evidence/audit-log',
|
route: '/evidence/audit-log',
|
||||||
menuGroupId: 'audit-evidence',
|
menuGroupId: 'audit-evidence',
|
||||||
menuGroupLabel: 'Audit & Evidence',
|
menuGroupLabel: 'Audit & Evidence',
|
||||||
@@ -939,7 +922,7 @@ export class AppSidebarComponent implements AfterViewInit {
|
|||||||
{
|
{
|
||||||
id: 'evidence-bundles',
|
id: 'evidence-bundles',
|
||||||
label: 'Bundles',
|
label: 'Bundles',
|
||||||
icon: 'archive',
|
icon: 'inbox',
|
||||||
route: '/triage/audit-bundles',
|
route: '/triage/audit-bundles',
|
||||||
menuGroupId: 'audit-evidence',
|
menuGroupId: 'audit-evidence',
|
||||||
menuGroupLabel: 'Audit & Evidence',
|
menuGroupLabel: 'Audit & Evidence',
|
||||||
@@ -948,6 +931,15 @@ export class AppSidebarComponent implements AfterViewInit {
|
|||||||
StellaOpsScopes.POLICY_AUDIT,
|
StellaOpsScopes.POLICY_AUDIT,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'evidence-trust-audit',
|
||||||
|
label: 'Trust Audit',
|
||||||
|
icon: 'shield-check',
|
||||||
|
route: '/evidence/audit-log/trust',
|
||||||
|
menuGroupId: 'audit-evidence',
|
||||||
|
menuGroupLabel: 'Audit & Evidence',
|
||||||
|
requireAnyScope: [StellaOpsScopes.SIGNER_READ],
|
||||||
|
},
|
||||||
// ── Group 5: Setup & Admin ───────────────────────────────────────
|
// ── Group 5: Setup & Admin ───────────────────────────────────────
|
||||||
{
|
{
|
||||||
id: 'setup-integrations',
|
id: 'setup-integrations',
|
||||||
@@ -972,8 +964,8 @@ export class AppSidebarComponent implements AfterViewInit {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'setup-trust-signing',
|
id: 'setup-trust-signing',
|
||||||
label: 'Trust & Signing',
|
label: 'Certificates',
|
||||||
icon: 'shield',
|
icon: 'key',
|
||||||
route: '/setup/trust-signing',
|
route: '/setup/trust-signing',
|
||||||
menuGroupId: 'setup-admin',
|
menuGroupId: 'setup-admin',
|
||||||
menuGroupLabel: 'Settings',
|
menuGroupLabel: 'Settings',
|
||||||
@@ -984,7 +976,7 @@ export class AppSidebarComponent implements AfterViewInit {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'setup-branding',
|
id: 'setup-branding',
|
||||||
label: 'Tenant & Branding',
|
label: 'Theme & Branding',
|
||||||
icon: 'paintbrush',
|
icon: 'paintbrush',
|
||||||
route: '/setup/tenant-branding',
|
route: '/setup/tenant-branding',
|
||||||
menuGroupId: 'setup-admin',
|
menuGroupId: 'setup-admin',
|
||||||
@@ -994,7 +986,7 @@ export class AppSidebarComponent implements AfterViewInit {
|
|||||||
{
|
{
|
||||||
id: 'setup-preferences',
|
id: 'setup-preferences',
|
||||||
label: 'User Preferences',
|
label: 'User Preferences',
|
||||||
icon: 'user',
|
icon: 'sliders',
|
||||||
route: '/setup/preferences',
|
route: '/setup/preferences',
|
||||||
menuGroupId: 'setup-admin',
|
menuGroupId: 'setup-admin',
|
||||||
menuGroupLabel: 'Settings',
|
menuGroupLabel: 'Settings',
|
||||||
@@ -1050,6 +1042,10 @@ export class AppSidebarComponent implements AfterViewInit {
|
|||||||
this.loadPendingApprovalsBadge(true);
|
this.loadPendingApprovalsBadge(true);
|
||||||
this.loadActionBadges();
|
this.loadActionBadges();
|
||||||
this.destroyRef.onDestroy(() => this.clearFlyoutTimers());
|
this.destroyRef.onDestroy(() => this.clearFlyoutTimers());
|
||||||
|
|
||||||
|
// Auto-expand the sidebar group matching the current URL on load
|
||||||
|
this.expandGroupForUrl(this.router.url);
|
||||||
|
|
||||||
this.router.events
|
this.router.events
|
||||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
.subscribe((event) => {
|
.subscribe((event) => {
|
||||||
@@ -1057,12 +1053,50 @@ export class AppSidebarComponent implements AfterViewInit {
|
|||||||
this.loadPendingApprovalsBadge(this.shouldForcePendingApprovalsRefresh(event.urlAfterRedirects));
|
this.loadPendingApprovalsBadge(this.shouldForcePendingApprovalsRefresh(event.urlAfterRedirects));
|
||||||
this.doctorTrendService.refresh();
|
this.doctorTrendService.refresh();
|
||||||
this.closeFlyout();
|
this.closeFlyout();
|
||||||
|
this.expandGroupForUrl(event.urlAfterRedirects);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Auto-expand the sidebar group that owns a route matching the given URL. */
|
||||||
|
private expandGroupForUrl(url: string): void {
|
||||||
|
const path = url.split('?')[0];
|
||||||
|
let bestMatch: NavSection | null = null;
|
||||||
|
let bestLength = 0;
|
||||||
|
for (const section of this.navSections) {
|
||||||
|
if (path.startsWith(section.route) && section.menuGroupId && section.route.length > bestLength) {
|
||||||
|
bestMatch = section;
|
||||||
|
bestLength = section.route.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bestMatch) {
|
||||||
|
this.sidebarPrefs.expandGroup(bestMatch.menuGroupId!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
ngAfterViewInit(): void {
|
||||||
this.setupEdgeAutoScroll();
|
this.setupEdgeAutoScroll();
|
||||||
|
// Poll for the active nav item and scroll it into view once the group-expand
|
||||||
|
// animation settles. Retries every 200ms for up to 2s to handle variable
|
||||||
|
// timing of CSS transitions, change detection, and routerLinkActive resolution.
|
||||||
|
this.pollScrollActiveItemIntoView(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private pollScrollActiveItemIntoView(attempt: number): void {
|
||||||
|
if (attempt > 10) return; // give up after 2s (10 × 200ms)
|
||||||
|
setTimeout(() => {
|
||||||
|
const navEl = this.sidebarNavRef?.nativeElement;
|
||||||
|
if (!navEl) return;
|
||||||
|
const active = navEl.querySelector('.nav-item--active') as HTMLElement | null;
|
||||||
|
if (!active) {
|
||||||
|
this.pollScrollActiveItemIntoView(attempt + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const navRect = navEl.getBoundingClientRect();
|
||||||
|
const activeRect = active.getBoundingClientRect();
|
||||||
|
if (activeRect.top >= navRect.top && activeRect.bottom <= navRect.bottom) return;
|
||||||
|
active.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||||
|
}, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
private filterSection(section: NavSection): NavSection | null {
|
private filterSection(section: NavSection): NavSection | null {
|
||||||
|
|||||||
Reference in New Issue
Block a user