diff --git a/src/Web/StellaOps.Web/src/app/features/jobengine/jobengine-dashboard.component.ts b/src/Web/StellaOps.Web/src/app/features/jobengine/jobengine-dashboard.component.ts index f3b797fc1..ac7b99330 100644 --- a/src/Web/StellaOps.Web/src/app/features/jobengine/jobengine-dashboard.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/jobengine/jobengine-dashboard.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, OnInit, inject, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnInit, computed, inject, signal } from '@angular/core'; import { RouterLink } from '@angular/router'; import { catchError, forkJoin, of } from 'rxjs'; @@ -58,6 +58,12 @@ import { OPERATIONS_PATHS } from '../platform/ops/operations-paths'; + @if (allCountsZero()) { +

+ No jobs have been submitted yet. Jobs are created automatically when releases are promoted, scans are triggered, or scheduled tasks run. +

+ } +

Jobs

@@ -237,6 +243,15 @@ import { OPERATIONS_PATHS } from '../platform/ops/operations-paths'; font-size: 0.85rem; } + .jobengine-dashboard__empty-hint { + margin: 0; + padding: 0.75rem 1rem; + color: var(--color-text-secondary); + font-size: 0.9rem; + line-height: 1.5; + border-left: 3px solid var(--color-border-primary); + } + .jobengine-dashboard__grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); @@ -339,6 +354,16 @@ export class JobEngineDashboardComponent implements OnInit { protected readonly jobSummary = signal(null); protected readonly quotaSummary = signal(null); protected readonly deadLetterStats = signal(null); + protected readonly allCountsZero = computed(() => { + const jobs = this.jobSummary(); + const quotas = this.quotaSummary(); + const dl = this.deadLetterStats(); + return !this.loading() + && (jobs?.totalJobs ?? 0) === 0 + && (quotas?.totalQuotas ?? 0) === 0 + && (dl?.totalEntries ?? 0) === 0; + }); + ngOnInit(): void { this.refresh(); } diff --git a/src/Web/StellaOps.Web/src/app/features/policy-simulation/shadow-mode-dashboard.component.spec.ts b/src/Web/StellaOps.Web/src/app/features/policy-simulation/shadow-mode-dashboard.component.spec.ts index 7bf0f3f4b..696b954b1 100644 --- a/src/Web/StellaOps.Web/src/app/features/policy-simulation/shadow-mode-dashboard.component.spec.ts +++ b/src/Web/StellaOps.Web/src/app/features/policy-simulation/shadow-mode-dashboard.component.spec.ts @@ -1,5 +1,6 @@ import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; import { Component, Input, Output, EventEmitter } from '@angular/core'; import { of, throwError } from 'rxjs'; import { delay } from 'rxjs/operators'; @@ -96,7 +97,7 @@ describe('ShadowModeDashboardComponent', () => { }) .overrideComponent(ShadowModeDashboardComponent, { set: { - imports: [MockShadowModeIndicatorComponent, ReactiveFormsModule], + imports: [MockShadowModeIndicatorComponent, ReactiveFormsModule, RouterTestingModule], providers: [{ provide: POLICY_SIMULATION_API, useValue: mockApi }], }, }) diff --git a/src/Web/StellaOps.Web/src/app/features/policy-simulation/shadow-mode-dashboard.component.ts b/src/Web/StellaOps.Web/src/app/features/policy-simulation/shadow-mode-dashboard.component.ts index ace057aa5..60e776fe1 100644 --- a/src/Web/StellaOps.Web/src/app/features/policy-simulation/shadow-mode-dashboard.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/policy-simulation/shadow-mode-dashboard.component.ts @@ -1,6 +1,7 @@ import { CommonModule } from '@angular/common'; import { Component, ChangeDetectionStrategy, inject, computed, OnInit } from '@angular/core'; import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; import { ShadowModeIndicatorComponent } from './shadow-mode-indicator.component'; import { ShadowModeStateService } from './shadow-mode-state.service'; @@ -12,7 +13,7 @@ import { ShadowModeStateService } from './shadow-mode-state.service'; */ @Component({ selector: 'app-shadow-mode-dashboard', - imports: [CommonModule, ReactiveFormsModule, ShadowModeIndicatorComponent], + imports: [CommonModule, ReactiveFormsModule, RouterModule, ShadowModeIndicatorComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
@@ -192,6 +193,10 @@ import { ShadowModeStateService } from './shadow-mode-state.service'; +

+ Shadow mode requires at least one active policy pack. + Create a pack in the Packs tab first. +

}
@@ -530,6 +535,23 @@ import { ShadowModeStateService } from './shadow-mode-state.service'; .shadow-dashboard__empty p { margin: 0 0 1.5rem; } + + .shadow-dashboard__prereq { + margin-top: 1rem; + font-size: 0.8rem; + color: var(--color-text-secondary); + line-height: 1.5; + } + + .shadow-dashboard__prereq a { + color: var(--color-status-info-border); + text-decoration: underline; + text-underline-offset: 2px; + } + + .shadow-dashboard__prereq a:hover { + color: var(--color-status-info); + } `, ] }) diff --git a/src/Web/StellaOps.Web/src/app/features/policy-simulation/shadow-mode-indicator.component.spec.ts b/src/Web/StellaOps.Web/src/app/features/policy-simulation/shadow-mode-indicator.component.spec.ts index c0257eb86..f3370b25c 100644 --- a/src/Web/StellaOps.Web/src/app/features/policy-simulation/shadow-mode-indicator.component.spec.ts +++ b/src/Web/StellaOps.Web/src/app/features/policy-simulation/shadow-mode-indicator.component.spec.ts @@ -1,4 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; import { ShadowModeIndicatorComponent } from './shadow-mode-indicator.component'; import { ShadowModeConfig } from '../../core/api/policy-simulation.models'; @@ -31,7 +32,7 @@ describe('ShadowModeIndicatorComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ShadowModeIndicatorComponent], + imports: [ShadowModeIndicatorComponent, RouterTestingModule], }).compileComponents(); fixture = TestBed.createComponent(ShadowModeIndicatorComponent); @@ -264,6 +265,47 @@ describe('ShadowModeIndicatorComponent', () => { }); }); + describe('Prerequisite Help Text', () => { + it('should show prerequisite text when disabled and actions visible', () => { + component.config = mockDisabledConfig; + component.showActions = true; + fixture.detectChanges(); + + const prereq = fixture.nativeElement.querySelector('.shadow-indicator__prereq'); + expect(prereq).toBeTruthy(); + expect(prereq.textContent).toContain('Shadow mode requires at least one active policy pack'); + }); + + it('should include link to packs page', () => { + component.config = mockDisabledConfig; + component.showActions = true; + fixture.detectChanges(); + + const link = fixture.nativeElement.querySelector('.shadow-indicator__prereq a'); + expect(link).toBeTruthy(); + expect(link.textContent).toContain('Create a pack in the Packs tab first.'); + expect(link.getAttribute('href')).toBe('/ops/policy/packs'); + }); + + it('should hide prerequisite text when enabled', () => { + component.config = mockEnabledConfig; + component.showActions = true; + fixture.detectChanges(); + + const prereq = fixture.nativeElement.querySelector('.shadow-indicator__prereq'); + expect(prereq).toBeFalsy(); + }); + + it('should hide prerequisite text when actions hidden', () => { + component.config = mockDisabledConfig; + component.showActions = false; + fixture.detectChanges(); + + const prereq = fixture.nativeElement.querySelector('.shadow-indicator__prereq'); + expect(prereq).toBeFalsy(); + }); + }); + describe('Loading State', () => { beforeEach(() => { component.config = mockDisabledConfig; diff --git a/src/Web/StellaOps.Web/src/app/features/policy-simulation/shadow-mode-indicator.component.ts b/src/Web/StellaOps.Web/src/app/features/policy-simulation/shadow-mode-indicator.component.ts index 3d84efe47..360a20df0 100644 --- a/src/Web/StellaOps.Web/src/app/features/policy-simulation/shadow-mode-indicator.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/policy-simulation/shadow-mode-indicator.component.ts @@ -1,5 +1,6 @@ import { Component, ChangeDetectionStrategy, Input, Output, EventEmitter } from '@angular/core'; +import { RouterModule } from '@angular/router'; import { ShadowModeConfig } from '../../core/api/policy-simulation.models'; @@ -10,7 +11,7 @@ import { ShadowModeConfig } from '../../core/api/policy-simulation.models'; */ @Component({ selector: 'app-shadow-mode-indicator', - imports: [], + imports: [RouterModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
} + + @if (showActions && !config?.enabled) { +

+ Shadow mode requires at least one active policy pack. + Create a pack in the Packs tab first. +

+ } `, styles: [ @@ -225,6 +233,24 @@ import { ShadowModeConfig } from '../../core/api/policy-simulation.models'; opacity: 0.5; cursor: not-allowed; } + + .shadow-indicator__prereq { + margin: 0; + font-size: 0.75rem; + color: var(--color-text-secondary); + line-height: 1.4; + max-width: 220px; + } + + .shadow-indicator__prereq a { + color: var(--color-status-info-border); + text-decoration: underline; + text-underline-offset: 2px; + } + + .shadow-indicator__prereq a:hover { + color: var(--color-status-info); + } `, ] }) diff --git a/src/Web/StellaOps.Web/src/app/features/policy-studio/workspace/policy-workspace.component.ts b/src/Web/StellaOps.Web/src/app/features/policy-studio/workspace/policy-workspace.component.ts index 14b5a3f4f..a00516800 100644 --- a/src/Web/StellaOps.Web/src/app/features/policy-studio/workspace/policy-workspace.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/policy-studio/workspace/policy-workspace.component.ts @@ -94,6 +94,19 @@ import { PolicyPackStore } from '../services/policy-pack.store'; + } @empty { + @if (!loading) { +
+

No policy packs configured

+

+ Policy packs define the rules that govern release decisions. + Create a pack to start defining your organization's release policy. +

+ + Learn about policy packs + +
+ } }