Compare commits
2 Commits
dev
...
059-unifie
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5dd79d45c2 | ||
|
|
ffd41693f8 |
@ -1,15 +1,17 @@
|
|||||||
<!--
|
<!--
|
||||||
Sync Impact Report
|
Sync Impact Report
|
||||||
|
|
||||||
- Version change: 1.2.0 → 1.2.1
|
- Version change: 1.2.1 → 1.3.0
|
||||||
- Modified principles:
|
- Modified principles:
|
||||||
- Operations / Run Observability Standard (clarify AuditLog vs OperationRun)
|
- None
|
||||||
|
- Added principles:
|
||||||
|
- Badge Semantics Are Centralized (BADGE-001)
|
||||||
- Added sections: None
|
- Added sections: None
|
||||||
- Removed sections: None
|
- Removed sections: None
|
||||||
- Templates requiring updates:
|
- Templates requiring updates:
|
||||||
- ✅ .specify/templates/plan-template.md
|
- ✅ .specify/templates/plan-template.md
|
||||||
- ✅ .specify/templates/tasks-template.md
|
|
||||||
- ✅ .specify/templates/spec-template.md
|
- ✅ .specify/templates/spec-template.md
|
||||||
|
- ✅ .specify/templates/tasks-template.md
|
||||||
- Follow-up TODOs:
|
- Follow-up TODOs:
|
||||||
- TODO(DELETED_STATUS): Keep “deleted” reserved for Feature 900 / Policy Lifecycle.
|
- TODO(DELETED_STATUS): Keep “deleted” reserved for Feature 900 / Policy Lifecycle.
|
||||||
-->
|
-->
|
||||||
@ -72,6 +74,12 @@ ### Data Minimization & Safe Logging
|
|||||||
- Payload-heavy content belongs in immutable snapshots/backup storage, not Inventory.
|
- Payload-heavy content belongs in immutable snapshots/backup storage, not Inventory.
|
||||||
- Logs MUST not contain secrets/tokens; monitoring MUST rely on run records + error codes (not log parsing).
|
- Logs MUST not contain secrets/tokens; monitoring MUST rely on run records + error codes (not log parsing).
|
||||||
|
|
||||||
|
### Badge Semantics Are Centralized (BADGE-001)
|
||||||
|
- Status-like badges (status/outcome/severity/risk/availability/boolean signals) MUST render via `BadgeCatalog` / `BadgeRenderer`.
|
||||||
|
- Filament resources/pages/widgets/views MUST NOT introduce ad-hoc status-like badge mappings (use a `BadgeDomain` instead).
|
||||||
|
- Introducing or changing a status-like value MUST include updating the relevant badge mapper and adding/updating tests for the mapping.
|
||||||
|
- Tag/category chips (e.g., type/platform/environment) are not status-like and are not governed by BADGE-001.
|
||||||
|
|
||||||
### Spec-First Workflow
|
### Spec-First Workflow
|
||||||
- For any feature that changes runtime behavior, include or update `specs/<NNN>-<slug>/` with `spec.md`, `plan.md`, `tasks.md`, and `checklists/requirements.md`.
|
- For any feature that changes runtime behavior, include or update `specs/<NNN>-<slug>/` with `spec.md`, `plan.md`, `tasks.md`, and `checklists/requirements.md`.
|
||||||
- New work branches from `dev` using `feat/<NNN>-<slug>` (spec + code in the same PR).
|
- New work branches from `dev` using `feat/<NNN>-<slug>` (spec + code in the same PR).
|
||||||
@ -96,4 +104,4 @@ ### Versioning Policy (SemVer)
|
|||||||
- **MINOR**: new principle/section or materially expanded guidance.
|
- **MINOR**: new principle/section or materially expanded guidance.
|
||||||
- **MAJOR**: removing/redefining principles in a backward-incompatible way.
|
- **MAJOR**: removing/redefining principles in a backward-incompatible way.
|
||||||
|
|
||||||
**Version**: 1.2.1 | **Ratified**: 2026-01-03 | **Last Amended**: 2026-01-17
|
**Version**: 1.3.0 | **Ratified**: 2026-01-03 | **Last Amended**: 2026-01-22
|
||||||
|
|||||||
@ -39,6 +39,7 @@ ## Constitution Check
|
|||||||
- Run observability: long-running/remote/queued work creates/reuses `OperationRun`; start surfaces enqueue-only; Monitoring is DB-only; DB-only <2s actions may skip runs but security-relevant ones still audit-log
|
- Run observability: long-running/remote/queued work creates/reuses `OperationRun`; start surfaces enqueue-only; Monitoring is DB-only; DB-only <2s actions may skip runs but security-relevant ones still audit-log
|
||||||
- Automation: queued/scheduled ops use locks + idempotency; handle 429/503 with backoff+jitter
|
- Automation: queued/scheduled ops use locks + idempotency; handle 429/503 with backoff+jitter
|
||||||
- Data minimization: Inventory stores metadata + whitelisted meta; logs contain no secrets/tokens
|
- Data minimization: Inventory stores metadata + whitelisted meta; logs contain no secrets/tokens
|
||||||
|
- Badge semantics (BADGE-001): status-like badges use `BadgeCatalog` / `BadgeRenderer`; no ad-hoc mappings; new values include tests
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
|
|||||||
@ -82,6 +82,9 @@ ## Requirements *(mandatory)*
|
|||||||
(preview/confirmation/audit), tenant isolation, run observability (`OperationRun` type/identity/visibility), and tests.
|
(preview/confirmation/audit), tenant isolation, run observability (`OperationRun` type/identity/visibility), and tests.
|
||||||
If security-relevant DB-only actions intentionally skip `OperationRun`, the spec MUST describe `AuditLog` entries.
|
If security-relevant DB-only actions intentionally skip `OperationRun`, the spec MUST describe `AuditLog` entries.
|
||||||
|
|
||||||
|
**Constitution alignment (BADGE-001):** If this feature changes status-like badges (status/outcome/severity/risk/availability/boolean),
|
||||||
|
the spec MUST describe how badge semantics stay centralized (no ad-hoc mappings) and which tests cover any new/changed values.
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
ACTION REQUIRED: The content in this section represents placeholders.
|
ACTION REQUIRED: The content in this section represents placeholders.
|
||||||
Fill them out with the right functional requirements.
|
Fill them out with the right functional requirements.
|
||||||
|
|||||||
@ -12,6 +12,8 @@ # Tasks: [FEATURE NAME]
|
|||||||
**Operations**: If this feature introduces long-running/remote/queued/scheduled work, include tasks to create/reuse and update a
|
**Operations**: If this feature introduces long-running/remote/queued/scheduled work, include tasks to create/reuse and update a
|
||||||
canonical `OperationRun`, and ensure “View run” links route to the canonical Monitoring hub.
|
canonical `OperationRun`, and ensure “View run” links route to the canonical Monitoring hub.
|
||||||
If security-relevant DB-only actions skip `OperationRun`, include tasks for `AuditLog` entries (before/after + actor + tenant).
|
If security-relevant DB-only actions skip `OperationRun`, include tasks for `AuditLog` entries (before/after + actor + tenant).
|
||||||
|
**Badges**: If this feature changes status-like badge semantics, tasks MUST use `BadgeCatalog` / `BadgeRenderer` (BADGE-001),
|
||||||
|
avoid ad-hoc mappings in Filament, and include mapping tests for any new/changed values.
|
||||||
|
|
||||||
**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.
|
**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.
|
||||||
|
|
||||||
|
|||||||
@ -1072,6 +1072,7 @@ ### Replaced Utilities
|
|||||||
## Active Technologies
|
## Active Technologies
|
||||||
- PHP 8.4.15 (Laravel 12) + Filament v4, Livewire v3 (054-unify-runs-suitewide-session-1768601416)
|
- PHP 8.4.15 (Laravel 12) + Filament v4, Livewire v3 (054-unify-runs-suitewide-session-1768601416)
|
||||||
- PostgreSQL (`operation_runs` + JSONB for summary/failures/context; partial unique index for active-run dedupe) (054-unify-runs-suitewide-session-1768601416)
|
- PostgreSQL (`operation_runs` + JSONB for summary/failures/context; partial unique index for active-run dedupe) (054-unify-runs-suitewide-session-1768601416)
|
||||||
|
- PHP 8.4.15 (Laravel 12) + Filament v5 + Livewire v4 (059-unified-badges)
|
||||||
|
|
||||||
## Recent Changes
|
## Recent Changes
|
||||||
- 054-unify-runs-suitewide-session-1768601416: Added PHP 8.4.15 (Laravel 12) + Filament v4, Livewire v3
|
- 054-unify-runs-suitewide-session-1768601416: Added PHP 8.4.15 (Laravel 12) + Filament v4, Livewire v3
|
||||||
|
|||||||
@ -3,6 +3,8 @@
|
|||||||
namespace App\Filament\Pages\Monitoring;
|
namespace App\Filament\Pages\Monitoring;
|
||||||
|
|
||||||
use App\Models\OperationRun;
|
use App\Models\OperationRun;
|
||||||
|
use App\Support\Badges\BadgeDomain;
|
||||||
|
use App\Support\Badges\BadgeRenderer;
|
||||||
use App\Support\OperationCatalog;
|
use App\Support\OperationCatalog;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Filament\Facades\Filament;
|
use Filament\Facades\Filament;
|
||||||
@ -51,21 +53,17 @@ public function table(Table $table): Table
|
|||||||
|
|
||||||
TextColumn::make('status')
|
TextColumn::make('status')
|
||||||
->badge()
|
->badge()
|
||||||
->colors([
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::OperationRunStatus))
|
||||||
'secondary' => 'queued',
|
->color(BadgeRenderer::color(BadgeDomain::OperationRunStatus))
|
||||||
'warning' => 'running',
|
->icon(BadgeRenderer::icon(BadgeDomain::OperationRunStatus))
|
||||||
'success' => 'completed',
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::OperationRunStatus)),
|
||||||
]),
|
|
||||||
|
|
||||||
TextColumn::make('outcome')
|
TextColumn::make('outcome')
|
||||||
->badge()
|
->badge()
|
||||||
->colors([
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::OperationRunOutcome))
|
||||||
'gray' => 'pending',
|
->color(BadgeRenderer::color(BadgeDomain::OperationRunOutcome))
|
||||||
'success' => 'succeeded',
|
->icon(BadgeRenderer::icon(BadgeDomain::OperationRunOutcome))
|
||||||
'warning' => 'partially_succeeded',
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::OperationRunOutcome)),
|
||||||
'danger' => 'failed',
|
|
||||||
'secondary' => 'cancelled',
|
|
||||||
]),
|
|
||||||
|
|
||||||
TextColumn::make('initiator_name')
|
TextColumn::make('initiator_name')
|
||||||
->label('Initiator')
|
->label('Initiator')
|
||||||
|
|||||||
@ -15,6 +15,8 @@
|
|||||||
use App\Services\BackupScheduling\ScheduleTimeService;
|
use App\Services\BackupScheduling\ScheduleTimeService;
|
||||||
use App\Services\Intune\AuditLogger;
|
use App\Services\Intune\AuditLogger;
|
||||||
use App\Services\OperationRunService;
|
use App\Services\OperationRunService;
|
||||||
|
use App\Support\Badges\BadgeDomain;
|
||||||
|
use App\Support\Badges\BadgeRenderer;
|
||||||
use App\Support\OperationRunLinks;
|
use App\Support\OperationRunLinks;
|
||||||
use App\Support\OpsUx\OperationUxPresenter;
|
use App\Support\OpsUx\OperationUxPresenter;
|
||||||
use App\Support\OpsUx\OpsUxBrowserEvents;
|
use App\Support\OpsUx\OpsUxBrowserEvents;
|
||||||
@ -215,23 +217,35 @@ public static function table(Table $table): Table
|
|||||||
TextColumn::make('last_run_status')
|
TextColumn::make('last_run_status')
|
||||||
->label('Last run status')
|
->label('Last run status')
|
||||||
->badge()
|
->badge()
|
||||||
->formatStateUsing(fn (?string $state): string => match ($state) {
|
->formatStateUsing(function (?string $state): string {
|
||||||
BackupScheduleRun::STATUS_RUNNING => 'Running',
|
if (! filled($state)) {
|
||||||
BackupScheduleRun::STATUS_SUCCESS => 'Success',
|
return '—';
|
||||||
BackupScheduleRun::STATUS_PARTIAL => 'Partial',
|
}
|
||||||
BackupScheduleRun::STATUS_FAILED => 'Failed',
|
|
||||||
BackupScheduleRun::STATUS_CANCELED => 'Canceled',
|
return BadgeRenderer::spec(BadgeDomain::BackupScheduleRunStatus, $state)->label;
|
||||||
BackupScheduleRun::STATUS_SKIPPED => 'Skipped',
|
|
||||||
default => $state ? Str::headline($state) : '—',
|
|
||||||
})
|
})
|
||||||
->color(fn (?string $state): string => match ($state) {
|
->color(function (?string $state): string {
|
||||||
BackupScheduleRun::STATUS_SUCCESS => 'success',
|
if (! filled($state)) {
|
||||||
BackupScheduleRun::STATUS_PARTIAL => 'warning',
|
return 'gray';
|
||||||
BackupScheduleRun::STATUS_RUNNING => 'primary',
|
}
|
||||||
BackupScheduleRun::STATUS_SKIPPED => 'gray',
|
|
||||||
BackupScheduleRun::STATUS_FAILED,
|
return BadgeRenderer::spec(BadgeDomain::BackupScheduleRunStatus, $state)->color;
|
||||||
BackupScheduleRun::STATUS_CANCELED => 'danger',
|
})
|
||||||
default => 'gray',
|
->icon(function (?string $state): ?string {
|
||||||
|
if (! filled($state)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return BadgeRenderer::spec(BadgeDomain::BackupScheduleRunStatus, $state)->icon;
|
||||||
|
})
|
||||||
|
->iconColor(function (?string $state): string {
|
||||||
|
if (! filled($state)) {
|
||||||
|
return 'gray';
|
||||||
|
}
|
||||||
|
|
||||||
|
$spec = BadgeRenderer::spec(BadgeDomain::BackupScheduleRunStatus, $state);
|
||||||
|
|
||||||
|
return $spec->iconColor ?? $spec->color;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
TextColumn::make('last_run_at')
|
TextColumn::make('last_run_at')
|
||||||
|
|||||||
@ -5,6 +5,8 @@
|
|||||||
use App\Filament\Resources\BackupSetResource;
|
use App\Filament\Resources\BackupSetResource;
|
||||||
use App\Models\BackupScheduleRun;
|
use App\Models\BackupScheduleRun;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
|
use App\Support\Badges\BadgeDomain;
|
||||||
|
use App\Support\Badges\BadgeRenderer;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
use Filament\Resources\RelationManagers\RelationManager;
|
use Filament\Resources\RelationManagers\RelationManager;
|
||||||
use Filament\Tables;
|
use Filament\Tables;
|
||||||
@ -27,15 +29,10 @@ public function table(Table $table): Table
|
|||||||
->dateTime(),
|
->dateTime(),
|
||||||
Tables\Columns\TextColumn::make('status')
|
Tables\Columns\TextColumn::make('status')
|
||||||
->badge()
|
->badge()
|
||||||
->color(fn (?string $state): string => match ($state) {
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::BackupScheduleRunStatus))
|
||||||
BackupScheduleRun::STATUS_SUCCESS => 'success',
|
->color(BadgeRenderer::color(BadgeDomain::BackupScheduleRunStatus))
|
||||||
BackupScheduleRun::STATUS_PARTIAL => 'warning',
|
->icon(BadgeRenderer::icon(BadgeDomain::BackupScheduleRunStatus))
|
||||||
BackupScheduleRun::STATUS_RUNNING => 'primary',
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::BackupScheduleRunStatus)),
|
||||||
BackupScheduleRun::STATUS_SKIPPED => 'gray',
|
|
||||||
BackupScheduleRun::STATUS_FAILED,
|
|
||||||
BackupScheduleRun::STATUS_CANCELED => 'danger',
|
|
||||||
default => 'gray',
|
|
||||||
}),
|
|
||||||
Tables\Columns\TextColumn::make('duration')
|
Tables\Columns\TextColumn::make('duration')
|
||||||
->label('Duration')
|
->label('Duration')
|
||||||
->getStateUsing(function (BackupScheduleRun $record): string {
|
->getStateUsing(function (BackupScheduleRun $record): string {
|
||||||
|
|||||||
@ -14,6 +14,8 @@
|
|||||||
use App\Services\Intune\BackupService;
|
use App\Services\Intune\BackupService;
|
||||||
use App\Services\OperationRunService;
|
use App\Services\OperationRunService;
|
||||||
use App\Services\Operations\BulkSelectionIdentity;
|
use App\Services\Operations\BulkSelectionIdentity;
|
||||||
|
use App\Support\Badges\BadgeDomain;
|
||||||
|
use App\Support\Badges\BadgeRenderer;
|
||||||
use App\Support\OperationRunLinks;
|
use App\Support\OperationRunLinks;
|
||||||
use App\Support\OpsUx\OperationUxPresenter;
|
use App\Support\OpsUx\OperationUxPresenter;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
@ -57,7 +59,12 @@ public static function table(Table $table): Table
|
|||||||
return $table
|
return $table
|
||||||
->columns([
|
->columns([
|
||||||
Tables\Columns\TextColumn::make('name')->searchable(),
|
Tables\Columns\TextColumn::make('name')->searchable(),
|
||||||
Tables\Columns\TextColumn::make('status')->badge(),
|
Tables\Columns\TextColumn::make('status')
|
||||||
|
->badge()
|
||||||
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::BackupSetStatus))
|
||||||
|
->color(BadgeRenderer::color(BadgeDomain::BackupSetStatus))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::BackupSetStatus))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::BackupSetStatus)),
|
||||||
Tables\Columns\TextColumn::make('item_count')->label('Items'),
|
Tables\Columns\TextColumn::make('item_count')->label('Items'),
|
||||||
Tables\Columns\TextColumn::make('created_by')->label('Created by'),
|
Tables\Columns\TextColumn::make('created_by')->label('Created by'),
|
||||||
Tables\Columns\TextColumn::make('completed_at')->dateTime()->since(),
|
Tables\Columns\TextColumn::make('completed_at')->dateTime()->since(),
|
||||||
@ -401,7 +408,12 @@ public static function infolist(Schema $schema): Schema
|
|||||||
return $schema
|
return $schema
|
||||||
->schema([
|
->schema([
|
||||||
Infolists\Components\TextEntry::make('name'),
|
Infolists\Components\TextEntry::make('name'),
|
||||||
Infolists\Components\TextEntry::make('status')->badge(),
|
Infolists\Components\TextEntry::make('status')
|
||||||
|
->badge()
|
||||||
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::BackupSetStatus))
|
||||||
|
->color(BadgeRenderer::color(BadgeDomain::BackupSetStatus))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::BackupSetStatus))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::BackupSetStatus)),
|
||||||
Infolists\Components\TextEntry::make('item_count')->label('Items'),
|
Infolists\Components\TextEntry::make('item_count')->label('Items'),
|
||||||
Infolists\Components\TextEntry::make('created_by')->label('Created by'),
|
Infolists\Components\TextEntry::make('created_by')->label('Created by'),
|
||||||
Infolists\Components\TextEntry::make('completed_at')->dateTime(),
|
Infolists\Components\TextEntry::make('completed_at')->dateTime(),
|
||||||
|
|||||||
@ -8,6 +8,8 @@
|
|||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\OperationRunService;
|
use App\Services\OperationRunService;
|
||||||
|
use App\Support\Badges\BadgeDomain;
|
||||||
|
use App\Support\Badges\BadgeRenderer;
|
||||||
use App\Support\OperationRunLinks;
|
use App\Support\OperationRunLinks;
|
||||||
use App\Support\OpsUx\OperationUxPresenter;
|
use App\Support\OpsUx\OperationUxPresenter;
|
||||||
use App\Support\OpsUx\OpsUxBrowserEvents;
|
use App\Support\OpsUx\OpsUxBrowserEvents;
|
||||||
@ -56,12 +58,18 @@ public function table(Table $table): Table
|
|||||||
->label('Restore')
|
->label('Restore')
|
||||||
->badge()
|
->badge()
|
||||||
->state(fn (BackupItem $record) => static::typeMeta($record->policy_type)['restore'] ?? 'enabled')
|
->state(fn (BackupItem $record) => static::typeMeta($record->policy_type)['restore'] ?? 'enabled')
|
||||||
->color(fn (?string $state) => $state === 'preview-only' ? 'warning' : 'success'),
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::PolicyRestoreMode))
|
||||||
|
->color(BadgeRenderer::color(BadgeDomain::PolicyRestoreMode))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::PolicyRestoreMode))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::PolicyRestoreMode)),
|
||||||
Tables\Columns\TextColumn::make('risk')
|
Tables\Columns\TextColumn::make('risk')
|
||||||
->label('Risk')
|
->label('Risk')
|
||||||
->badge()
|
->badge()
|
||||||
->state(fn (BackupItem $record) => static::typeMeta($record->policy_type)['risk'] ?? 'n/a')
|
->state(fn (BackupItem $record) => static::typeMeta($record->policy_type)['risk'] ?? 'n/a')
|
||||||
->color(fn (?string $state) => str_contains((string) $state, 'high') ? 'danger' : 'gray'),
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::PolicyRisk))
|
||||||
|
->color(BadgeRenderer::color(BadgeDomain::PolicyRisk))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::PolicyRisk))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::PolicyRisk)),
|
||||||
Tables\Columns\TextColumn::make('policy_identifier')
|
Tables\Columns\TextColumn::make('policy_identifier')
|
||||||
->label('Policy ID')
|
->label('Policy ID')
|
||||||
->copyable(),
|
->copyable(),
|
||||||
|
|||||||
@ -5,6 +5,8 @@
|
|||||||
use App\Filament\Resources\EntraGroupSyncRunResource\Pages;
|
use App\Filament\Resources\EntraGroupSyncRunResource\Pages;
|
||||||
use App\Models\EntraGroupSyncRun;
|
use App\Models\EntraGroupSyncRun;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
|
use App\Support\Badges\BadgeDomain;
|
||||||
|
use App\Support\Badges\BadgeRenderer;
|
||||||
use App\Support\OperationRunLinks;
|
use App\Support\OperationRunLinks;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
@ -60,7 +62,10 @@ public static function infolist(Schema $schema): Schema
|
|||||||
->placeholder('—'),
|
->placeholder('—'),
|
||||||
TextEntry::make('status')
|
TextEntry::make('status')
|
||||||
->badge()
|
->badge()
|
||||||
->color(fn (EntraGroupSyncRun $record): string => static::statusColor($record->status)),
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::EntraGroupSyncRunStatus))
|
||||||
|
->color(BadgeRenderer::color(BadgeDomain::EntraGroupSyncRunStatus))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::EntraGroupSyncRunStatus))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::EntraGroupSyncRunStatus)),
|
||||||
TextEntry::make('selection_key')->label('Selection'),
|
TextEntry::make('selection_key')->label('Selection'),
|
||||||
TextEntry::make('slot_key')->label('Slot')->placeholder('—')->copyable(),
|
TextEntry::make('slot_key')->label('Slot')->placeholder('—')->copyable(),
|
||||||
TextEntry::make('started_at')->dateTime(),
|
TextEntry::make('started_at')->dateTime(),
|
||||||
@ -106,7 +111,10 @@ public static function table(Table $table): Table
|
|||||||
->toggleable(),
|
->toggleable(),
|
||||||
Tables\Columns\TextColumn::make('status')
|
Tables\Columns\TextColumn::make('status')
|
||||||
->badge()
|
->badge()
|
||||||
->color(fn (EntraGroupSyncRun $record): string => static::statusColor($record->status)),
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::EntraGroupSyncRunStatus))
|
||||||
|
->color(BadgeRenderer::color(BadgeDomain::EntraGroupSyncRunStatus))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::EntraGroupSyncRunStatus))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::EntraGroupSyncRunStatus)),
|
||||||
Tables\Columns\TextColumn::make('selection_key')
|
Tables\Columns\TextColumn::make('selection_key')
|
||||||
->label('Selection')
|
->label('Selection')
|
||||||
->limit(24)
|
->limit(24)
|
||||||
@ -143,16 +151,4 @@ public static function getPages(): array
|
|||||||
'view' => Pages\ViewEntraGroupSyncRun::route('/{record}'),
|
'view' => Pages\ViewEntraGroupSyncRun::route('/{record}'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function statusColor(?string $status): string
|
|
||||||
{
|
|
||||||
return match ($status) {
|
|
||||||
EntraGroupSyncRun::STATUS_SUCCEEDED => 'success',
|
|
||||||
EntraGroupSyncRun::STATUS_PARTIAL => 'warning',
|
|
||||||
EntraGroupSyncRun::STATUS_FAILED => 'danger',
|
|
||||||
EntraGroupSyncRun::STATUS_RUNNING => 'info',
|
|
||||||
EntraGroupSyncRun::STATUS_PENDING => 'gray',
|
|
||||||
default => 'gray',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,8 @@
|
|||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\Drift\DriftFindingDiffBuilder;
|
use App\Services\Drift\DriftFindingDiffBuilder;
|
||||||
|
use App\Support\Badges\BadgeDomain;
|
||||||
|
use App\Support\Badges\BadgeRenderer;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
use Filament\Actions\BulkAction;
|
use Filament\Actions\BulkAction;
|
||||||
@ -50,8 +52,18 @@ public static function infolist(Schema $schema): Schema
|
|||||||
Section::make('Finding')
|
Section::make('Finding')
|
||||||
->schema([
|
->schema([
|
||||||
TextEntry::make('finding_type')->badge()->label('Type'),
|
TextEntry::make('finding_type')->badge()->label('Type'),
|
||||||
TextEntry::make('status')->badge(),
|
TextEntry::make('status')
|
||||||
TextEntry::make('severity')->badge(),
|
->badge()
|
||||||
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::FindingStatus))
|
||||||
|
->color(BadgeRenderer::color(BadgeDomain::FindingStatus))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::FindingStatus))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::FindingStatus)),
|
||||||
|
TextEntry::make('severity')
|
||||||
|
->badge()
|
||||||
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::FindingSeverity))
|
||||||
|
->color(BadgeRenderer::color(BadgeDomain::FindingSeverity))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::FindingSeverity))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::FindingSeverity)),
|
||||||
TextEntry::make('fingerprint')->label('Fingerprint')->copyable(),
|
TextEntry::make('fingerprint')->label('Fingerprint')->copyable(),
|
||||||
TextEntry::make('scope_key')->label('Scope')->copyable(),
|
TextEntry::make('scope_key')->label('Scope')->copyable(),
|
||||||
TextEntry::make('subject_display_name')->label('Subject')->placeholder('—'),
|
TextEntry::make('subject_display_name')->label('Subject')->placeholder('—'),
|
||||||
@ -188,8 +200,18 @@ public static function table(Table $table): Table
|
|||||||
->defaultSort('created_at', 'desc')
|
->defaultSort('created_at', 'desc')
|
||||||
->columns([
|
->columns([
|
||||||
Tables\Columns\TextColumn::make('finding_type')->badge()->label('Type'),
|
Tables\Columns\TextColumn::make('finding_type')->badge()->label('Type'),
|
||||||
Tables\Columns\TextColumn::make('status')->badge(),
|
Tables\Columns\TextColumn::make('status')
|
||||||
Tables\Columns\TextColumn::make('severity')->badge(),
|
->badge()
|
||||||
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::FindingStatus))
|
||||||
|
->color(BadgeRenderer::color(BadgeDomain::FindingStatus))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::FindingStatus))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::FindingStatus)),
|
||||||
|
Tables\Columns\TextColumn::make('severity')
|
||||||
|
->badge()
|
||||||
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::FindingSeverity))
|
||||||
|
->color(BadgeRenderer::color(BadgeDomain::FindingSeverity))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::FindingSeverity))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::FindingSeverity)),
|
||||||
Tables\Columns\TextColumn::make('subject_display_name')->label('Subject')->placeholder('—'),
|
Tables\Columns\TextColumn::make('subject_display_name')->label('Subject')->placeholder('—'),
|
||||||
Tables\Columns\TextColumn::make('subject_type')->label('Subject type')->searchable(),
|
Tables\Columns\TextColumn::make('subject_type')->label('Subject type')->searchable(),
|
||||||
Tables\Columns\TextColumn::make('subject_external_id')->label('External ID')->toggleable(isToggledHiddenByDefault: true),
|
Tables\Columns\TextColumn::make('subject_external_id')->label('External ID')->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
|||||||
@ -6,6 +6,8 @@
|
|||||||
use App\Filament\Resources\InventorySyncRunResource\Pages;
|
use App\Filament\Resources\InventorySyncRunResource\Pages;
|
||||||
use App\Models\InventorySyncRun;
|
use App\Models\InventorySyncRun;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
|
use App\Support\Badges\BadgeDomain;
|
||||||
|
use App\Support\Badges\BadgeRenderer;
|
||||||
use App\Support\OperationRunLinks;
|
use App\Support\OperationRunLinks;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
@ -66,14 +68,23 @@ public static function infolist(Schema $schema): Schema
|
|||||||
->placeholder('—'),
|
->placeholder('—'),
|
||||||
TextEntry::make('status')
|
TextEntry::make('status')
|
||||||
->badge()
|
->badge()
|
||||||
->color(fn (InventorySyncRun $record): string => static::statusColor($record->status)),
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::InventorySyncRunStatus))
|
||||||
|
->color(BadgeRenderer::color(BadgeDomain::InventorySyncRunStatus))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::InventorySyncRunStatus))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::InventorySyncRunStatus)),
|
||||||
TextEntry::make('selection_hash')->label('Selection hash')->copyable(),
|
TextEntry::make('selection_hash')->label('Selection hash')->copyable(),
|
||||||
TextEntry::make('started_at')->dateTime(),
|
TextEntry::make('started_at')->dateTime(),
|
||||||
TextEntry::make('finished_at')->dateTime(),
|
TextEntry::make('finished_at')->dateTime(),
|
||||||
TextEntry::make('items_observed_count')->label('Observed')->numeric(),
|
TextEntry::make('items_observed_count')->label('Observed')->numeric(),
|
||||||
TextEntry::make('items_upserted_count')->label('Upserted')->numeric(),
|
TextEntry::make('items_upserted_count')->label('Upserted')->numeric(),
|
||||||
TextEntry::make('errors_count')->label('Errors')->numeric(),
|
TextEntry::make('errors_count')->label('Errors')->numeric(),
|
||||||
TextEntry::make('had_errors')->label('Had errors')->badge(),
|
TextEntry::make('had_errors')
|
||||||
|
->label('Had errors')
|
||||||
|
->badge()
|
||||||
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::BooleanHasErrors))
|
||||||
|
->color(BadgeRenderer::color(BadgeDomain::BooleanHasErrors))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::BooleanHasErrors))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::BooleanHasErrors)),
|
||||||
])
|
])
|
||||||
->columns(2)
|
->columns(2)
|
||||||
->columnSpanFull(),
|
->columnSpanFull(),
|
||||||
@ -116,7 +127,10 @@ public static function table(Table $table): Table
|
|||||||
->toggleable(),
|
->toggleable(),
|
||||||
Tables\Columns\TextColumn::make('status')
|
Tables\Columns\TextColumn::make('status')
|
||||||
->badge()
|
->badge()
|
||||||
->color(fn (InventorySyncRun $record): string => static::statusColor($record->status)),
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::InventorySyncRunStatus))
|
||||||
|
->color(BadgeRenderer::color(BadgeDomain::InventorySyncRunStatus))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::InventorySyncRunStatus))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::InventorySyncRunStatus)),
|
||||||
Tables\Columns\TextColumn::make('selection_hash')
|
Tables\Columns\TextColumn::make('selection_hash')
|
||||||
->label('Selection')
|
->label('Selection')
|
||||||
->copyable()
|
->copyable()
|
||||||
@ -155,16 +169,4 @@ public static function getPages(): array
|
|||||||
'view' => Pages\ViewInventorySyncRun::route('/{record}'),
|
'view' => Pages\ViewInventorySyncRun::route('/{record}'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function statusColor(?string $status): string
|
|
||||||
{
|
|
||||||
return match ($status) {
|
|
||||||
InventorySyncRun::STATUS_SUCCESS => 'success',
|
|
||||||
InventorySyncRun::STATUS_PARTIAL => 'warning',
|
|
||||||
InventorySyncRun::STATUS_FAILED => 'danger',
|
|
||||||
InventorySyncRun::STATUS_SKIPPED => 'gray',
|
|
||||||
InventorySyncRun::STATUS_RUNNING => 'info',
|
|
||||||
default => 'gray',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,8 @@
|
|||||||
use App\Filament\Resources\OperationRunResource\Pages;
|
use App\Filament\Resources\OperationRunResource\Pages;
|
||||||
use App\Models\OperationRun;
|
use App\Models\OperationRun;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
|
use App\Support\Badges\BadgeDomain;
|
||||||
|
use App\Support\Badges\BadgeRenderer;
|
||||||
use App\Support\OperationCatalog;
|
use App\Support\OperationCatalog;
|
||||||
use App\Support\OperationRunOutcome;
|
use App\Support\OperationRunOutcome;
|
||||||
use App\Support\OperationRunStatus;
|
use App\Support\OperationRunStatus;
|
||||||
@ -63,10 +65,16 @@ public static function infolist(Schema $schema): Schema
|
|||||||
->formatStateUsing(fn (?string $state): string => OperationCatalog::label((string) $state)),
|
->formatStateUsing(fn (?string $state): string => OperationCatalog::label((string) $state)),
|
||||||
TextEntry::make('status')
|
TextEntry::make('status')
|
||||||
->badge()
|
->badge()
|
||||||
->color(fn (OperationRun $record): string => static::statusColor($record->status)),
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::OperationRunStatus))
|
||||||
|
->color(BadgeRenderer::color(BadgeDomain::OperationRunStatus))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::OperationRunStatus))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::OperationRunStatus)),
|
||||||
TextEntry::make('outcome')
|
TextEntry::make('outcome')
|
||||||
->badge()
|
->badge()
|
||||||
->color(fn (OperationRun $record): string => static::outcomeColor($record->outcome)),
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::OperationRunOutcome))
|
||||||
|
->color(BadgeRenderer::color(BadgeDomain::OperationRunOutcome))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::OperationRunOutcome))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::OperationRunOutcome)),
|
||||||
TextEntry::make('initiator_name')->label('Initiator'),
|
TextEntry::make('initiator_name')->label('Initiator'),
|
||||||
TextEntry::make('target_scope_display')
|
TextEntry::make('target_scope_display')
|
||||||
->label('Target')
|
->label('Target')
|
||||||
@ -147,7 +155,10 @@ public static function table(Table $table): Table
|
|||||||
->columns([
|
->columns([
|
||||||
Tables\Columns\TextColumn::make('status')
|
Tables\Columns\TextColumn::make('status')
|
||||||
->badge()
|
->badge()
|
||||||
->color(fn (OperationRun $record): string => static::statusColor($record->status)),
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::OperationRunStatus))
|
||||||
|
->color(BadgeRenderer::color(BadgeDomain::OperationRunStatus))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::OperationRunStatus))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::OperationRunStatus)),
|
||||||
Tables\Columns\TextColumn::make('type')
|
Tables\Columns\TextColumn::make('type')
|
||||||
->label('Operation')
|
->label('Operation')
|
||||||
->formatStateUsing(fn (?string $state): string => OperationCatalog::label((string) $state))
|
->formatStateUsing(fn (?string $state): string => OperationCatalog::label((string) $state))
|
||||||
@ -170,7 +181,10 @@ public static function table(Table $table): Table
|
|||||||
}),
|
}),
|
||||||
Tables\Columns\TextColumn::make('outcome')
|
Tables\Columns\TextColumn::make('outcome')
|
||||||
->badge()
|
->badge()
|
||||||
->color(fn (OperationRun $record): string => static::outcomeColor($record->outcome)),
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::OperationRunOutcome))
|
||||||
|
->color(BadgeRenderer::color(BadgeDomain::OperationRunOutcome))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::OperationRunOutcome))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::OperationRunOutcome)),
|
||||||
])
|
])
|
||||||
->filters([
|
->filters([
|
||||||
Tables\Filters\SelectFilter::make('type')
|
Tables\Filters\SelectFilter::make('type')
|
||||||
@ -256,27 +270,6 @@ public static function getPages(): array
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function statusColor(?string $status): string
|
|
||||||
{
|
|
||||||
return match ($status) {
|
|
||||||
'queued' => 'secondary',
|
|
||||||
'running' => 'warning',
|
|
||||||
'completed' => 'success',
|
|
||||||
default => 'gray',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function outcomeColor(?string $outcome): string
|
|
||||||
{
|
|
||||||
return match ($outcome) {
|
|
||||||
'succeeded' => 'success',
|
|
||||||
'partially_succeeded' => 'warning',
|
|
||||||
'failed' => 'danger',
|
|
||||||
'cancelled' => 'gray',
|
|
||||||
default => 'gray',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function targetScopeDisplay(OperationRun $record): ?string
|
private static function targetScopeDisplay(OperationRun $record): ?string
|
||||||
{
|
{
|
||||||
$context = is_array($record->context) ? $record->context : [];
|
$context = is_array($record->context) ? $record->context : [];
|
||||||
|
|||||||
@ -14,6 +14,8 @@
|
|||||||
use App\Services\Intune\PolicyNormalizer;
|
use App\Services\Intune\PolicyNormalizer;
|
||||||
use App\Services\OperationRunService;
|
use App\Services\OperationRunService;
|
||||||
use App\Services\Operations\BulkSelectionIdentity;
|
use App\Services\Operations\BulkSelectionIdentity;
|
||||||
|
use App\Support\Badges\BadgeDomain;
|
||||||
|
use App\Support\Badges\BadgeRenderer;
|
||||||
use App\Support\OperationRunLinks;
|
use App\Support\OperationRunLinks;
|
||||||
use App\Support\OpsUx\OperationUxPresenter;
|
use App\Support\OpsUx\OperationUxPresenter;
|
||||||
use App\Support\OpsUx\OpsUxBrowserEvents;
|
use App\Support\OpsUx\OpsUxBrowserEvents;
|
||||||
@ -66,8 +68,11 @@ public static function infolist(Schema $schema): Schema
|
|||||||
TextEntry::make('latest_snapshot_mode')
|
TextEntry::make('latest_snapshot_mode')
|
||||||
->label('Snapshot')
|
->label('Snapshot')
|
||||||
->badge()
|
->badge()
|
||||||
->color(fn (Policy $record): string => (static::latestVersionMetadata($record)['source'] ?? null) === 'metadata_only' ? 'warning' : 'success')
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::PolicySnapshotMode))
|
||||||
->state(fn (Policy $record): string => (static::latestVersionMetadata($record)['source'] ?? null) === 'metadata_only' ? 'metadata only' : 'full')
|
->color(BadgeRenderer::color(BadgeDomain::PolicySnapshotMode))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::PolicySnapshotMode))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::PolicySnapshotMode))
|
||||||
|
->state(fn (Policy $record): string => (static::latestVersionMetadata($record)['source'] ?? null) === 'metadata_only' ? 'metadata_only' : 'full')
|
||||||
->helperText(function (Policy $record): ?string {
|
->helperText(function (Policy $record): ?string {
|
||||||
$meta = static::latestVersionMetadata($record);
|
$meta = static::latestVersionMetadata($record);
|
||||||
|
|
||||||
|
|||||||
@ -21,6 +21,8 @@
|
|||||||
use App\Services\Intune\RestoreService;
|
use App\Services\Intune\RestoreService;
|
||||||
use App\Services\OperationRunService;
|
use App\Services\OperationRunService;
|
||||||
use App\Services\Operations\BulkSelectionIdentity;
|
use App\Services\Operations\BulkSelectionIdentity;
|
||||||
|
use App\Support\Badges\BadgeDomain;
|
||||||
|
use App\Support\Badges\BadgeRenderer;
|
||||||
use App\Support\OperationRunLinks;
|
use App\Support\OperationRunLinks;
|
||||||
use App\Support\OpsUx\OperationUxPresenter;
|
use App\Support\OpsUx\OperationUxPresenter;
|
||||||
use App\Support\OpsUx\OpsUxBrowserEvents;
|
use App\Support\OpsUx\OpsUxBrowserEvents;
|
||||||
@ -709,7 +711,12 @@ public static function table(Table $table): Table
|
|||||||
->columns([
|
->columns([
|
||||||
Tables\Columns\TextColumn::make('backupSet.name')->label('Backup set'),
|
Tables\Columns\TextColumn::make('backupSet.name')->label('Backup set'),
|
||||||
Tables\Columns\IconColumn::make('is_dry_run')->label('Dry-run')->boolean(),
|
Tables\Columns\IconColumn::make('is_dry_run')->label('Dry-run')->boolean(),
|
||||||
Tables\Columns\TextColumn::make('status')->badge(),
|
Tables\Columns\TextColumn::make('status')
|
||||||
|
->badge()
|
||||||
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::RestoreRunStatus))
|
||||||
|
->color(BadgeRenderer::color(BadgeDomain::RestoreRunStatus))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::RestoreRunStatus))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::RestoreRunStatus)),
|
||||||
Tables\Columns\TextColumn::make('summary_total')
|
Tables\Columns\TextColumn::make('summary_total')
|
||||||
->label('Total')
|
->label('Total')
|
||||||
->state(fn (RestoreRun $record): int => (int) (($record->metadata ?? [])['total'] ?? 0)),
|
->state(fn (RestoreRun $record): int => (int) (($record->metadata ?? [])['total'] ?? 0)),
|
||||||
@ -1251,7 +1258,12 @@ public static function infolist(Schema $schema): Schema
|
|||||||
return $schema
|
return $schema
|
||||||
->schema([
|
->schema([
|
||||||
Infolists\Components\TextEntry::make('backupSet.name')->label('Backup set'),
|
Infolists\Components\TextEntry::make('backupSet.name')->label('Backup set'),
|
||||||
Infolists\Components\TextEntry::make('status')->badge(),
|
Infolists\Components\TextEntry::make('status')
|
||||||
|
->badge()
|
||||||
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::RestoreRunStatus))
|
||||||
|
->color(BadgeRenderer::color(BadgeDomain::RestoreRunStatus))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::RestoreRunStatus))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::RestoreRunStatus)),
|
||||||
Infolists\Components\TextEntry::make('counts')
|
Infolists\Components\TextEntry::make('counts')
|
||||||
->label('Counts')
|
->label('Counts')
|
||||||
->state(function (RestoreRun $record): string {
|
->state(function (RestoreRun $record): string {
|
||||||
|
|||||||
@ -17,6 +17,8 @@
|
|||||||
use App\Services\Intune\TenantPermissionService;
|
use App\Services\Intune\TenantPermissionService;
|
||||||
use App\Services\OperationRunService;
|
use App\Services\OperationRunService;
|
||||||
use App\Services\Operations\BulkSelectionIdentity;
|
use App\Services\Operations\BulkSelectionIdentity;
|
||||||
|
use App\Support\Badges\BadgeDomain;
|
||||||
|
use App\Support\Badges\BadgeRenderer;
|
||||||
use App\Support\OperationRunLinks;
|
use App\Support\OperationRunLinks;
|
||||||
use App\Support\OpsUx\OperationUxPresenter;
|
use App\Support\OpsUx\OperationUxPresenter;
|
||||||
use App\Support\OpsUx\OpsUxBrowserEvents;
|
use App\Support\OpsUx\OpsUxBrowserEvents;
|
||||||
@ -149,9 +151,17 @@ public static function table(Table $table): Table
|
|||||||
->boolean(),
|
->boolean(),
|
||||||
Tables\Columns\TextColumn::make('status')
|
Tables\Columns\TextColumn::make('status')
|
||||||
->badge()
|
->badge()
|
||||||
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::TenantStatus))
|
||||||
|
->color(BadgeRenderer::color(BadgeDomain::TenantStatus))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::TenantStatus))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::TenantStatus))
|
||||||
->sortable(),
|
->sortable(),
|
||||||
Tables\Columns\TextColumn::make('app_status')
|
Tables\Columns\TextColumn::make('app_status')
|
||||||
->badge(),
|
->badge()
|
||||||
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::TenantAppStatus))
|
||||||
|
->color(BadgeRenderer::color(BadgeDomain::TenantAppStatus))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::TenantAppStatus))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::TenantAppStatus)),
|
||||||
Tables\Columns\TextColumn::make('created_at')
|
Tables\Columns\TextColumn::make('created_at')
|
||||||
->dateTime()
|
->dateTime()
|
||||||
->since(),
|
->since(),
|
||||||
@ -501,35 +511,26 @@ public static function infolist(Schema $schema): Schema
|
|||||||
Infolists\Components\TextEntry::make('app_client_id')->label('App Client ID')->copyable(),
|
Infolists\Components\TextEntry::make('app_client_id')->label('App Client ID')->copyable(),
|
||||||
Infolists\Components\TextEntry::make('status')
|
Infolists\Components\TextEntry::make('status')
|
||||||
->badge()
|
->badge()
|
||||||
->color(fn (string $state): string => match ($state) {
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::TenantStatus))
|
||||||
'active' => 'success',
|
->color(BadgeRenderer::color(BadgeDomain::TenantStatus))
|
||||||
'inactive' => 'gray',
|
->icon(BadgeRenderer::icon(BadgeDomain::TenantStatus))
|
||||||
'suspended' => 'warning',
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::TenantStatus)),
|
||||||
'error' => 'danger',
|
|
||||||
default => 'gray',
|
|
||||||
}),
|
|
||||||
Infolists\Components\TextEntry::make('app_status')
|
Infolists\Components\TextEntry::make('app_status')
|
||||||
->badge()
|
->badge()
|
||||||
->color(fn (string $state): string => match ($state) {
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::TenantAppStatus))
|
||||||
'ok', 'configured' => 'success',
|
->color(BadgeRenderer::color(BadgeDomain::TenantAppStatus))
|
||||||
'pending' => 'warning',
|
->icon(BadgeRenderer::icon(BadgeDomain::TenantAppStatus))
|
||||||
'error' => 'danger',
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::TenantAppStatus)),
|
||||||
'requires_consent' => 'warning',
|
|
||||||
default => 'gray',
|
|
||||||
}),
|
|
||||||
Infolists\Components\TextEntry::make('app_notes')->label('Notes'),
|
Infolists\Components\TextEntry::make('app_notes')->label('Notes'),
|
||||||
Infolists\Components\TextEntry::make('created_at')->dateTime(),
|
Infolists\Components\TextEntry::make('created_at')->dateTime(),
|
||||||
Infolists\Components\TextEntry::make('updated_at')->dateTime(),
|
Infolists\Components\TextEntry::make('updated_at')->dateTime(),
|
||||||
Infolists\Components\TextEntry::make('rbac_status')
|
Infolists\Components\TextEntry::make('rbac_status')
|
||||||
->label('RBAC status')
|
->label('RBAC status')
|
||||||
->badge()
|
->badge()
|
||||||
->color(fn (string $state): string => match ($state) {
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::TenantRbacStatus))
|
||||||
'ok', 'configured' => 'success',
|
->color(BadgeRenderer::color(BadgeDomain::TenantRbacStatus))
|
||||||
'manual_assignment_required' => 'warning',
|
->icon(BadgeRenderer::icon(BadgeDomain::TenantRbacStatus))
|
||||||
'error', 'failed' => 'danger',
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::TenantRbacStatus)),
|
||||||
'not_configured' => 'gray',
|
|
||||||
default => 'gray',
|
|
||||||
}),
|
|
||||||
Infolists\Components\TextEntry::make('rbac_status_reason')->label('RBAC reason'),
|
Infolists\Components\TextEntry::make('rbac_status_reason')->label('RBAC reason'),
|
||||||
Infolists\Components\TextEntry::make('rbac_last_checked_at')->label('RBAC last checked')->since(),
|
Infolists\Components\TextEntry::make('rbac_last_checked_at')->label('RBAC last checked')->since(),
|
||||||
Infolists\Components\TextEntry::make('rbac_role_display_name')->label('RBAC role'),
|
Infolists\Components\TextEntry::make('rbac_role_display_name')->label('RBAC role'),
|
||||||
@ -558,12 +559,10 @@ public static function infolist(Schema $schema): Schema
|
|||||||
->formatStateUsing(fn ($state) => is_array($state) ? implode(', ', $state) : (string) $state),
|
->formatStateUsing(fn ($state) => is_array($state) ? implode(', ', $state) : (string) $state),
|
||||||
Infolists\Components\TextEntry::make('status')
|
Infolists\Components\TextEntry::make('status')
|
||||||
->badge()
|
->badge()
|
||||||
->color(fn (string $state): string => match ($state) {
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::TenantPermissionStatus))
|
||||||
'granted' => 'success',
|
->color(BadgeRenderer::color(BadgeDomain::TenantPermissionStatus))
|
||||||
'missing' => 'warning',
|
->icon(BadgeRenderer::icon(BadgeDomain::TenantPermissionStatus))
|
||||||
'error' => 'danger',
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::TenantPermissionStatus)),
|
||||||
default => 'gray',
|
|
||||||
}),
|
|
||||||
])
|
])
|
||||||
->columnSpanFull(),
|
->columnSpanFull(),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -8,6 +8,8 @@
|
|||||||
use App\Models\Finding;
|
use App\Models\Finding;
|
||||||
use App\Models\InventoryItem;
|
use App\Models\InventoryItem;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
|
use App\Support\Badges\BadgeDomain;
|
||||||
|
use App\Support\Badges\BadgeRenderer;
|
||||||
use App\Support\OpsUx\ActiveRuns;
|
use App\Support\OpsUx\ActiveRuns;
|
||||||
use Filament\Facades\Filament;
|
use Filament\Facades\Filament;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
@ -41,14 +43,16 @@ public function table(Table $table): Table
|
|||||||
->tooltip(fn (Finding $record): ?string => $record->subject_display_name ?: null),
|
->tooltip(fn (Finding $record): ?string => $record->subject_display_name ?: null),
|
||||||
TextColumn::make('severity')
|
TextColumn::make('severity')
|
||||||
->badge()
|
->badge()
|
||||||
->color(fn (Finding $record): string => match ($record->severity) {
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::FindingSeverity))
|
||||||
Finding::SEVERITY_HIGH => 'danger',
|
->color(BadgeRenderer::color(BadgeDomain::FindingSeverity))
|
||||||
Finding::SEVERITY_MEDIUM => 'warning',
|
->icon(BadgeRenderer::icon(BadgeDomain::FindingSeverity))
|
||||||
default => 'gray',
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::FindingSeverity)),
|
||||||
}),
|
|
||||||
TextColumn::make('status')
|
TextColumn::make('status')
|
||||||
->badge()
|
->badge()
|
||||||
->color(fn (Finding $record): string => $record->status === Finding::STATUS_NEW ? 'warning' : 'gray'),
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::FindingStatus))
|
||||||
|
->color(BadgeRenderer::color(BadgeDomain::FindingStatus))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::FindingStatus))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::FindingStatus)),
|
||||||
TextColumn::make('created_at')
|
TextColumn::make('created_at')
|
||||||
->label('Created')
|
->label('Created')
|
||||||
->since(),
|
->since(),
|
||||||
|
|||||||
@ -6,6 +6,8 @@
|
|||||||
|
|
||||||
use App\Models\OperationRun;
|
use App\Models\OperationRun;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
|
use App\Support\Badges\BadgeDomain;
|
||||||
|
use App\Support\Badges\BadgeRenderer;
|
||||||
use App\Support\OperationCatalog;
|
use App\Support\OperationCatalog;
|
||||||
use App\Support\OperationRunLinks;
|
use App\Support\OperationRunLinks;
|
||||||
use App\Support\OpsUx\ActiveRuns;
|
use App\Support\OpsUx\ActiveRuns;
|
||||||
@ -43,10 +45,16 @@ public function table(Table $table): Table
|
|||||||
->tooltip(fn (OperationRun $record): string => OperationCatalog::label((string) $record->type)),
|
->tooltip(fn (OperationRun $record): string => OperationCatalog::label((string) $record->type)),
|
||||||
TextColumn::make('status')
|
TextColumn::make('status')
|
||||||
->badge()
|
->badge()
|
||||||
->color(fn (OperationRun $record): string => $this->statusColor($record->status)),
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::OperationRunStatus))
|
||||||
|
->color(BadgeRenderer::color(BadgeDomain::OperationRunStatus))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::OperationRunStatus))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::OperationRunStatus)),
|
||||||
TextColumn::make('outcome')
|
TextColumn::make('outcome')
|
||||||
->badge()
|
->badge()
|
||||||
->color(fn (OperationRun $record): string => $this->outcomeColor($record->outcome)),
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::OperationRunOutcome))
|
||||||
|
->color(BadgeRenderer::color(BadgeDomain::OperationRunOutcome))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::OperationRunOutcome))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::OperationRunOutcome)),
|
||||||
TextColumn::make('created_at')
|
TextColumn::make('created_at')
|
||||||
->label('Started')
|
->label('Started')
|
||||||
->since(),
|
->since(),
|
||||||
@ -70,25 +78,4 @@ private function getQuery(): Builder
|
|||||||
->when($tenantId, fn (Builder $query) => $query->where('tenant_id', $tenantId))
|
->when($tenantId, fn (Builder $query) => $query->where('tenant_id', $tenantId))
|
||||||
->latest('created_at');
|
->latest('created_at');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function statusColor(?string $status): string
|
|
||||||
{
|
|
||||||
return match ($status) {
|
|
||||||
'queued' => 'secondary',
|
|
||||||
'running' => 'warning',
|
|
||||||
'completed' => 'success',
|
|
||||||
default => 'gray',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private function outcomeColor(?string $outcome): string
|
|
||||||
{
|
|
||||||
return match ($outcome) {
|
|
||||||
'succeeded' => 'success',
|
|
||||||
'partially_succeeded' => 'warning',
|
|
||||||
'failed' => 'danger',
|
|
||||||
'cancelled' => 'gray',
|
|
||||||
default => 'gray',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,8 @@
|
|||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\OperationRunService;
|
use App\Services\OperationRunService;
|
||||||
use App\Services\Operations\BulkSelectionIdentity;
|
use App\Services\Operations\BulkSelectionIdentity;
|
||||||
|
use App\Support\Badges\BadgeDomain;
|
||||||
|
use App\Support\Badges\BadgeRenderer;
|
||||||
use App\Support\OperationRunLinks;
|
use App\Support\OperationRunLinks;
|
||||||
use App\Support\OpsUx\OperationUxPresenter;
|
use App\Support\OpsUx\OperationUxPresenter;
|
||||||
use App\Support\OpsUx\OpsUxBrowserEvents;
|
use App\Support\OpsUx\OpsUxBrowserEvents;
|
||||||
@ -107,8 +109,10 @@ public function table(Table $table): Table
|
|||||||
TextColumn::make('ignored_at')
|
TextColumn::make('ignored_at')
|
||||||
->label('Ignored')
|
->label('Ignored')
|
||||||
->badge()
|
->badge()
|
||||||
->color(fn (?string $state): string => filled($state) ? 'warning' : 'gray')
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::IgnoredAt))
|
||||||
->formatStateUsing(fn (?string $state): string => filled($state) ? 'yes' : 'no')
|
->color(BadgeRenderer::color(BadgeDomain::IgnoredAt))
|
||||||
|
->icon(BadgeRenderer::icon(BadgeDomain::IgnoredAt))
|
||||||
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::IgnoredAt))
|
||||||
->toggleable(isToggledHiddenByDefault: true),
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
])
|
])
|
||||||
->modifyQueryUsing(fn (Builder $query) => $query->withCount('versions'))
|
->modifyQueryUsing(fn (Builder $query) => $query->withCount('versions'))
|
||||||
|
|||||||
122
app/Support/Badges/BadgeCatalog.php
Normal file
122
app/Support/Badges/BadgeCatalog.php
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support\Badges;
|
||||||
|
|
||||||
|
use BackedEnum;
|
||||||
|
use Stringable;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
final class BadgeCatalog
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array<string, class-string<BadgeMapper>>
|
||||||
|
*/
|
||||||
|
private const DOMAIN_MAPPERS = [
|
||||||
|
BadgeDomain::OperationRunStatus->value => Domains\OperationRunStatusBadge::class,
|
||||||
|
BadgeDomain::OperationRunOutcome->value => Domains\OperationRunOutcomeBadge::class,
|
||||||
|
BadgeDomain::InventorySyncRunStatus->value => Domains\InventorySyncRunStatusBadge::class,
|
||||||
|
BadgeDomain::BackupScheduleRunStatus->value => Domains\BackupScheduleRunStatusBadge::class,
|
||||||
|
BadgeDomain::BackupSetStatus->value => Domains\BackupSetStatusBadge::class,
|
||||||
|
BadgeDomain::EntraGroupSyncRunStatus->value => Domains\EntraGroupSyncRunStatusBadge::class,
|
||||||
|
BadgeDomain::RestoreRunStatus->value => Domains\RestoreRunStatusBadge::class,
|
||||||
|
BadgeDomain::RestoreCheckSeverity->value => Domains\RestoreCheckSeverityBadge::class,
|
||||||
|
BadgeDomain::FindingStatus->value => Domains\FindingStatusBadge::class,
|
||||||
|
BadgeDomain::FindingSeverity->value => Domains\FindingSeverityBadge::class,
|
||||||
|
BadgeDomain::BooleanEnabled->value => Domains\BooleanEnabledBadge::class,
|
||||||
|
BadgeDomain::BooleanHasErrors->value => Domains\BooleanHasErrorsBadge::class,
|
||||||
|
BadgeDomain::TenantStatus->value => Domains\TenantStatusBadge::class,
|
||||||
|
BadgeDomain::TenantAppStatus->value => Domains\TenantAppStatusBadge::class,
|
||||||
|
BadgeDomain::TenantRbacStatus->value => Domains\TenantRbacStatusBadge::class,
|
||||||
|
BadgeDomain::TenantPermissionStatus->value => Domains\TenantPermissionStatusBadge::class,
|
||||||
|
BadgeDomain::PolicySnapshotMode->value => Domains\PolicySnapshotModeBadge::class,
|
||||||
|
BadgeDomain::PolicyRestoreMode->value => Domains\PolicyRestoreModeBadge::class,
|
||||||
|
BadgeDomain::PolicyRisk->value => Domains\PolicyRiskBadge::class,
|
||||||
|
BadgeDomain::IgnoredAt->value => Domains\IgnoredAtBadge::class,
|
||||||
|
BadgeDomain::RestorePreviewDecision->value => Domains\RestorePreviewDecisionBadge::class,
|
||||||
|
BadgeDomain::RestoreResultStatus->value => Domains\RestoreResultStatusBadge::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, BadgeMapper|null>
|
||||||
|
*/
|
||||||
|
private static array $mapperCache = [];
|
||||||
|
|
||||||
|
public static function spec(BadgeDomain $domain, mixed $value): BadgeSpec
|
||||||
|
{
|
||||||
|
$mapper = self::mapper($domain);
|
||||||
|
|
||||||
|
if (! $mapper) {
|
||||||
|
return BadgeSpec::unknown();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return $mapper->spec($value);
|
||||||
|
} catch (Throwable) {
|
||||||
|
return BadgeSpec::unknown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function mapper(BadgeDomain $domain): ?BadgeMapper
|
||||||
|
{
|
||||||
|
$key = $domain->value;
|
||||||
|
|
||||||
|
if (array_key_exists($key, self::$mapperCache)) {
|
||||||
|
return self::$mapperCache[$key];
|
||||||
|
}
|
||||||
|
|
||||||
|
$mapper = self::buildMapper($domain);
|
||||||
|
|
||||||
|
self::$mapperCache[$key] = $mapper;
|
||||||
|
|
||||||
|
return $mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function normalizeState(mixed $value): ?string
|
||||||
|
{
|
||||||
|
if ($value === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof BackedEnum) {
|
||||||
|
$value = $value->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof Stringable) {
|
||||||
|
$value = (string) $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_bool($value)) {
|
||||||
|
return $value ? 'true' : 'false';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_int($value) || is_float($value)) {
|
||||||
|
return (string) $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! is_string($value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized = strtolower(trim($value));
|
||||||
|
$normalized = str_replace([' ', '-'], '_', $normalized);
|
||||||
|
|
||||||
|
return $normalized === '' ? null : $normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function buildMapper(BadgeDomain $domain): ?BadgeMapper
|
||||||
|
{
|
||||||
|
$mapperClass = self::DOMAIN_MAPPERS[$domain->value] ?? null;
|
||||||
|
|
||||||
|
if (! $mapperClass) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! class_exists($mapperClass)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mapper = new $mapperClass;
|
||||||
|
|
||||||
|
return $mapper instanceof BadgeMapper ? $mapper : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
29
app/Support/Badges/BadgeDomain.php
Normal file
29
app/Support/Badges/BadgeDomain.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support\Badges;
|
||||||
|
|
||||||
|
enum BadgeDomain: string
|
||||||
|
{
|
||||||
|
case OperationRunStatus = 'operation_run_status';
|
||||||
|
case OperationRunOutcome = 'operation_run_outcome';
|
||||||
|
case InventorySyncRunStatus = 'inventory_sync_run_status';
|
||||||
|
case BackupScheduleRunStatus = 'backup_schedule_run_status';
|
||||||
|
case BackupSetStatus = 'backup_set_status';
|
||||||
|
case EntraGroupSyncRunStatus = 'entra_group_sync_run_status';
|
||||||
|
case RestoreRunStatus = 'restore_run_status';
|
||||||
|
case RestoreCheckSeverity = 'restore_check_severity';
|
||||||
|
case FindingStatus = 'finding_status';
|
||||||
|
case FindingSeverity = 'finding_severity';
|
||||||
|
case BooleanEnabled = 'boolean_enabled';
|
||||||
|
case BooleanHasErrors = 'boolean_has_errors';
|
||||||
|
case TenantStatus = 'tenant_status';
|
||||||
|
case TenantAppStatus = 'tenant_app_status';
|
||||||
|
case TenantRbacStatus = 'tenant_rbac_status';
|
||||||
|
case TenantPermissionStatus = 'tenant_permission_status';
|
||||||
|
case PolicySnapshotMode = 'policy_snapshot_mode';
|
||||||
|
case PolicyRestoreMode = 'policy_restore_mode';
|
||||||
|
case PolicyRisk = 'policy_risk';
|
||||||
|
case IgnoredAt = 'ignored_at';
|
||||||
|
case RestorePreviewDecision = 'restore_preview_decision';
|
||||||
|
case RestoreResultStatus = 'restore_result_status';
|
||||||
|
}
|
||||||
8
app/Support/Badges/BadgeMapper.php
Normal file
8
app/Support/Badges/BadgeMapper.php
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support\Badges;
|
||||||
|
|
||||||
|
interface BadgeMapper
|
||||||
|
{
|
||||||
|
public function spec(mixed $value): BadgeSpec;
|
||||||
|
}
|
||||||
43
app/Support/Badges/BadgeRenderer.php
Normal file
43
app/Support/Badges/BadgeRenderer.php
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support\Badges;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
|
||||||
|
final class BadgeRenderer
|
||||||
|
{
|
||||||
|
public static function label(BadgeDomain $domain): Closure
|
||||||
|
{
|
||||||
|
return static function (mixed $state, mixed ...$args) use ($domain): string {
|
||||||
|
return BadgeCatalog::spec($domain, $state)->label;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function color(BadgeDomain $domain): Closure
|
||||||
|
{
|
||||||
|
return static function (mixed $state, mixed ...$args) use ($domain): string {
|
||||||
|
return BadgeCatalog::spec($domain, $state)->color;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function icon(BadgeDomain $domain): Closure
|
||||||
|
{
|
||||||
|
return static function (mixed $state, mixed ...$args) use ($domain): ?string {
|
||||||
|
return BadgeCatalog::spec($domain, $state)->icon;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function iconColor(BadgeDomain $domain): Closure
|
||||||
|
{
|
||||||
|
return static function (mixed $state, mixed ...$args) use ($domain): ?string {
|
||||||
|
$spec = BadgeCatalog::spec($domain, $state);
|
||||||
|
|
||||||
|
return $spec->iconColor ?? $spec->color;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function spec(BadgeDomain $domain, mixed $state): BadgeSpec
|
||||||
|
{
|
||||||
|
return BadgeCatalog::spec($domain, $state);
|
||||||
|
}
|
||||||
|
}
|
||||||
61
app/Support/Badges/BadgeSpec.php
Normal file
61
app/Support/Badges/BadgeSpec.php
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support\Badges;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
final class BadgeSpec
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array<int, string>
|
||||||
|
*/
|
||||||
|
private const ALLOWED_COLORS = [
|
||||||
|
'gray',
|
||||||
|
'info',
|
||||||
|
'success',
|
||||||
|
'warning',
|
||||||
|
'danger',
|
||||||
|
'primary',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public readonly string $label,
|
||||||
|
public readonly string $color,
|
||||||
|
public readonly ?string $icon = null,
|
||||||
|
public readonly ?string $iconColor = null,
|
||||||
|
) {
|
||||||
|
if (trim($this->label) === '') {
|
||||||
|
throw new InvalidArgumentException('BadgeSpec label must be a non-empty string.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! in_array($this->color, self::ALLOWED_COLORS, true)) {
|
||||||
|
throw new InvalidArgumentException('BadgeSpec color must be one of: '.implode(', ', self::ALLOWED_COLORS));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->icon !== null && trim($this->icon) === '') {
|
||||||
|
throw new InvalidArgumentException('BadgeSpec icon must be null or a non-empty string.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->iconColor !== null && ! in_array($this->iconColor, self::ALLOWED_COLORS, true)) {
|
||||||
|
throw new InvalidArgumentException('BadgeSpec iconColor must be null or one of: '.implode(', ', self::ALLOWED_COLORS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public static function allowedColors(): array
|
||||||
|
{
|
||||||
|
return self::ALLOWED_COLORS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function unknown(): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
label: 'Unknown',
|
||||||
|
color: 'gray',
|
||||||
|
icon: 'heroicon-m-question-mark-circle',
|
||||||
|
iconColor: 'gray',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
app/Support/Badges/Domains/BackupScheduleRunStatusBadge.php
Normal file
26
app/Support/Badges/Domains/BackupScheduleRunStatusBadge.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support\Badges\Domains;
|
||||||
|
|
||||||
|
use App\Models\BackupScheduleRun;
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeMapper;
|
||||||
|
use App\Support\Badges\BadgeSpec;
|
||||||
|
|
||||||
|
final class BackupScheduleRunStatusBadge implements BadgeMapper
|
||||||
|
{
|
||||||
|
public function spec(mixed $value): BadgeSpec
|
||||||
|
{
|
||||||
|
$state = BadgeCatalog::normalizeState($value);
|
||||||
|
|
||||||
|
return match ($state) {
|
||||||
|
BackupScheduleRun::STATUS_RUNNING => new BadgeSpec('Running', 'info', 'heroicon-m-arrow-path'),
|
||||||
|
BackupScheduleRun::STATUS_SUCCESS => new BadgeSpec('Success', 'success', 'heroicon-m-check-circle'),
|
||||||
|
BackupScheduleRun::STATUS_PARTIAL => new BadgeSpec('Partial', 'warning', 'heroicon-m-exclamation-triangle'),
|
||||||
|
BackupScheduleRun::STATUS_FAILED => new BadgeSpec('Failed', 'danger', 'heroicon-m-x-circle'),
|
||||||
|
BackupScheduleRun::STATUS_CANCELED => new BadgeSpec('Canceled', 'gray', 'heroicon-m-minus-circle'),
|
||||||
|
BackupScheduleRun::STATUS_SKIPPED => new BadgeSpec('Skipped', 'gray', 'heroicon-m-minus-circle'),
|
||||||
|
default => BadgeSpec::unknown(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
23
app/Support/Badges/Domains/BackupSetStatusBadge.php
Normal file
23
app/Support/Badges/Domains/BackupSetStatusBadge.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support\Badges\Domains;
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeMapper;
|
||||||
|
use App\Support\Badges\BadgeSpec;
|
||||||
|
|
||||||
|
final class BackupSetStatusBadge implements BadgeMapper
|
||||||
|
{
|
||||||
|
public function spec(mixed $value): BadgeSpec
|
||||||
|
{
|
||||||
|
$state = BadgeCatalog::normalizeState($value);
|
||||||
|
|
||||||
|
return match ($state) {
|
||||||
|
'running' => new BadgeSpec('Running', 'info', 'heroicon-m-arrow-path'),
|
||||||
|
'completed' => new BadgeSpec('Completed', 'success', 'heroicon-m-check-circle'),
|
||||||
|
'partial' => new BadgeSpec('Partial', 'warning', 'heroicon-m-exclamation-triangle'),
|
||||||
|
'failed' => new BadgeSpec('Failed', 'danger', 'heroicon-m-x-circle'),
|
||||||
|
default => BadgeSpec::unknown(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
21
app/Support/Badges/Domains/BooleanEnabledBadge.php
Normal file
21
app/Support/Badges/Domains/BooleanEnabledBadge.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support\Badges\Domains;
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeMapper;
|
||||||
|
use App\Support\Badges\BadgeSpec;
|
||||||
|
|
||||||
|
final class BooleanEnabledBadge implements BadgeMapper
|
||||||
|
{
|
||||||
|
public function spec(mixed $value): BadgeSpec
|
||||||
|
{
|
||||||
|
$state = BadgeCatalog::normalizeState($value);
|
||||||
|
|
||||||
|
return match ($state) {
|
||||||
|
'true', '1', 'yes', 'enabled' => new BadgeSpec('Enabled', 'success', 'heroicon-m-check-circle'),
|
||||||
|
'false', '0', 'no', 'disabled' => new BadgeSpec('Disabled', 'gray', 'heroicon-m-minus-circle'),
|
||||||
|
default => BadgeSpec::unknown(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
21
app/Support/Badges/Domains/BooleanHasErrorsBadge.php
Normal file
21
app/Support/Badges/Domains/BooleanHasErrorsBadge.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support\Badges\Domains;
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeMapper;
|
||||||
|
use App\Support\Badges\BadgeSpec;
|
||||||
|
|
||||||
|
final class BooleanHasErrorsBadge implements BadgeMapper
|
||||||
|
{
|
||||||
|
public function spec(mixed $value): BadgeSpec
|
||||||
|
{
|
||||||
|
$state = BadgeCatalog::normalizeState($value);
|
||||||
|
|
||||||
|
return match ($state) {
|
||||||
|
'true', '1', 'yes', 'errors' => new BadgeSpec('Errors', 'danger', 'heroicon-m-x-circle'),
|
||||||
|
'false', '0', 'no' => new BadgeSpec('No errors', 'success', 'heroicon-m-check-circle'),
|
||||||
|
default => BadgeSpec::unknown(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
25
app/Support/Badges/Domains/EntraGroupSyncRunStatusBadge.php
Normal file
25
app/Support/Badges/Domains/EntraGroupSyncRunStatusBadge.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support\Badges\Domains;
|
||||||
|
|
||||||
|
use App\Models\EntraGroupSyncRun;
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeMapper;
|
||||||
|
use App\Support\Badges\BadgeSpec;
|
||||||
|
|
||||||
|
final class EntraGroupSyncRunStatusBadge implements BadgeMapper
|
||||||
|
{
|
||||||
|
public function spec(mixed $value): BadgeSpec
|
||||||
|
{
|
||||||
|
$state = BadgeCatalog::normalizeState($value);
|
||||||
|
|
||||||
|
return match ($state) {
|
||||||
|
EntraGroupSyncRun::STATUS_PENDING => new BadgeSpec('Pending', 'gray', 'heroicon-m-clock'),
|
||||||
|
EntraGroupSyncRun::STATUS_RUNNING => new BadgeSpec('Running', 'info', 'heroicon-m-arrow-path'),
|
||||||
|
EntraGroupSyncRun::STATUS_SUCCEEDED => new BadgeSpec('Succeeded', 'success', 'heroicon-m-check-circle'),
|
||||||
|
EntraGroupSyncRun::STATUS_PARTIAL => new BadgeSpec('Partial', 'warning', 'heroicon-m-exclamation-triangle'),
|
||||||
|
EntraGroupSyncRun::STATUS_FAILED => new BadgeSpec('Failed', 'danger', 'heroicon-m-x-circle'),
|
||||||
|
default => BadgeSpec::unknown(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
23
app/Support/Badges/Domains/FindingSeverityBadge.php
Normal file
23
app/Support/Badges/Domains/FindingSeverityBadge.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support\Badges\Domains;
|
||||||
|
|
||||||
|
use App\Models\Finding;
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeMapper;
|
||||||
|
use App\Support\Badges\BadgeSpec;
|
||||||
|
|
||||||
|
final class FindingSeverityBadge implements BadgeMapper
|
||||||
|
{
|
||||||
|
public function spec(mixed $value): BadgeSpec
|
||||||
|
{
|
||||||
|
$state = BadgeCatalog::normalizeState($value);
|
||||||
|
|
||||||
|
return match ($state) {
|
||||||
|
Finding::SEVERITY_LOW => new BadgeSpec('Low', 'gray', 'heroicon-m-minus-circle'),
|
||||||
|
Finding::SEVERITY_MEDIUM => new BadgeSpec('Medium', 'warning', 'heroicon-m-exclamation-triangle'),
|
||||||
|
Finding::SEVERITY_HIGH => new BadgeSpec('High', 'danger', 'heroicon-m-x-circle'),
|
||||||
|
default => BadgeSpec::unknown(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
22
app/Support/Badges/Domains/FindingStatusBadge.php
Normal file
22
app/Support/Badges/Domains/FindingStatusBadge.php
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support\Badges\Domains;
|
||||||
|
|
||||||
|
use App\Models\Finding;
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeMapper;
|
||||||
|
use App\Support\Badges\BadgeSpec;
|
||||||
|
|
||||||
|
final class FindingStatusBadge implements BadgeMapper
|
||||||
|
{
|
||||||
|
public function spec(mixed $value): BadgeSpec
|
||||||
|
{
|
||||||
|
$state = BadgeCatalog::normalizeState($value);
|
||||||
|
|
||||||
|
return match ($state) {
|
||||||
|
Finding::STATUS_NEW => new BadgeSpec('New', 'warning', 'heroicon-m-clock'),
|
||||||
|
Finding::STATUS_ACKNOWLEDGED => new BadgeSpec('Acknowledged', 'gray', 'heroicon-m-check-circle'),
|
||||||
|
default => BadgeSpec::unknown(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
21
app/Support/Badges/Domains/IgnoredAtBadge.php
Normal file
21
app/Support/Badges/Domains/IgnoredAtBadge.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support\Badges\Domains;
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeMapper;
|
||||||
|
use App\Support\Badges\BadgeSpec;
|
||||||
|
|
||||||
|
final class IgnoredAtBadge implements BadgeMapper
|
||||||
|
{
|
||||||
|
public function spec(mixed $value): BadgeSpec
|
||||||
|
{
|
||||||
|
$state = BadgeCatalog::normalizeState($value);
|
||||||
|
|
||||||
|
if ($state === null) {
|
||||||
|
return new BadgeSpec('No', 'gray', 'heroicon-m-minus-circle');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new BadgeSpec('Yes', 'warning', 'heroicon-m-exclamation-triangle');
|
||||||
|
}
|
||||||
|
}
|
||||||
26
app/Support/Badges/Domains/InventorySyncRunStatusBadge.php
Normal file
26
app/Support/Badges/Domains/InventorySyncRunStatusBadge.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support\Badges\Domains;
|
||||||
|
|
||||||
|
use App\Models\InventorySyncRun;
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeMapper;
|
||||||
|
use App\Support\Badges\BadgeSpec;
|
||||||
|
|
||||||
|
final class InventorySyncRunStatusBadge implements BadgeMapper
|
||||||
|
{
|
||||||
|
public function spec(mixed $value): BadgeSpec
|
||||||
|
{
|
||||||
|
$state = BadgeCatalog::normalizeState($value);
|
||||||
|
|
||||||
|
return match ($state) {
|
||||||
|
InventorySyncRun::STATUS_PENDING, 'queued' => new BadgeSpec('Pending', 'gray', 'heroicon-m-clock'),
|
||||||
|
InventorySyncRun::STATUS_RUNNING => new BadgeSpec('Running', 'info', 'heroicon-m-arrow-path'),
|
||||||
|
InventorySyncRun::STATUS_SUCCESS => new BadgeSpec('Success', 'success', 'heroicon-m-check-circle'),
|
||||||
|
InventorySyncRun::STATUS_PARTIAL => new BadgeSpec('Partial', 'warning', 'heroicon-m-exclamation-triangle'),
|
||||||
|
InventorySyncRun::STATUS_FAILED => new BadgeSpec('Failed', 'danger', 'heroicon-m-x-circle'),
|
||||||
|
InventorySyncRun::STATUS_SKIPPED => new BadgeSpec('Skipped', 'gray', 'heroicon-m-minus-circle'),
|
||||||
|
default => BadgeSpec::unknown(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
25
app/Support/Badges/Domains/OperationRunOutcomeBadge.php
Normal file
25
app/Support/Badges/Domains/OperationRunOutcomeBadge.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support\Badges\Domains;
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeMapper;
|
||||||
|
use App\Support\Badges\BadgeSpec;
|
||||||
|
use App\Support\OperationRunOutcome;
|
||||||
|
|
||||||
|
final class OperationRunOutcomeBadge implements BadgeMapper
|
||||||
|
{
|
||||||
|
public function spec(mixed $value): BadgeSpec
|
||||||
|
{
|
||||||
|
$state = BadgeCatalog::normalizeState($value);
|
||||||
|
|
||||||
|
return match ($state) {
|
||||||
|
OperationRunOutcome::Pending->value => new BadgeSpec('Pending', 'gray', 'heroicon-m-clock'),
|
||||||
|
OperationRunOutcome::Succeeded->value => new BadgeSpec('Succeeded', 'success', 'heroicon-m-check-circle'),
|
||||||
|
OperationRunOutcome::PartiallySucceeded->value => new BadgeSpec('Partially succeeded', 'warning', 'heroicon-m-exclamation-triangle'),
|
||||||
|
OperationRunOutcome::Failed->value => new BadgeSpec('Failed', 'danger', 'heroicon-m-x-circle'),
|
||||||
|
OperationRunOutcome::Cancelled->value => new BadgeSpec('Cancelled', 'gray', 'heroicon-m-minus-circle'),
|
||||||
|
default => BadgeSpec::unknown(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
23
app/Support/Badges/Domains/OperationRunStatusBadge.php
Normal file
23
app/Support/Badges/Domains/OperationRunStatusBadge.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support\Badges\Domains;
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeMapper;
|
||||||
|
use App\Support\Badges\BadgeSpec;
|
||||||
|
use App\Support\OperationRunStatus;
|
||||||
|
|
||||||
|
final class OperationRunStatusBadge implements BadgeMapper
|
||||||
|
{
|
||||||
|
public function spec(mixed $value): BadgeSpec
|
||||||
|
{
|
||||||
|
$state = BadgeCatalog::normalizeState($value);
|
||||||
|
|
||||||
|
return match ($state) {
|
||||||
|
OperationRunStatus::Queued->value => new BadgeSpec('Queued', 'warning', 'heroicon-m-clock'),
|
||||||
|
OperationRunStatus::Running->value => new BadgeSpec('Running', 'info', 'heroicon-m-arrow-path'),
|
||||||
|
OperationRunStatus::Completed->value => new BadgeSpec('Completed', 'gray', 'heroicon-m-check-circle'),
|
||||||
|
default => BadgeSpec::unknown(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
21
app/Support/Badges/Domains/PolicyRestoreModeBadge.php
Normal file
21
app/Support/Badges/Domains/PolicyRestoreModeBadge.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support\Badges\Domains;
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeMapper;
|
||||||
|
use App\Support\Badges\BadgeSpec;
|
||||||
|
|
||||||
|
final class PolicyRestoreModeBadge implements BadgeMapper
|
||||||
|
{
|
||||||
|
public function spec(mixed $value): BadgeSpec
|
||||||
|
{
|
||||||
|
$state = BadgeCatalog::normalizeState($value);
|
||||||
|
|
||||||
|
return match ($state) {
|
||||||
|
'enabled' => new BadgeSpec('Enabled', 'success', 'heroicon-m-check-circle'),
|
||||||
|
'preview_only' => new BadgeSpec('Preview only', 'warning', 'heroicon-m-exclamation-triangle'),
|
||||||
|
default => BadgeSpec::unknown(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
25
app/Support/Badges/Domains/PolicyRiskBadge.php
Normal file
25
app/Support/Badges/Domains/PolicyRiskBadge.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support\Badges\Domains;
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeMapper;
|
||||||
|
use App\Support\Badges\BadgeSpec;
|
||||||
|
|
||||||
|
final class PolicyRiskBadge implements BadgeMapper
|
||||||
|
{
|
||||||
|
public function spec(mixed $value): BadgeSpec
|
||||||
|
{
|
||||||
|
$state = BadgeCatalog::normalizeState($value);
|
||||||
|
|
||||||
|
return match ($state) {
|
||||||
|
'low' => new BadgeSpec('Low', 'gray', 'heroicon-m-minus-circle'),
|
||||||
|
'low_medium' => new BadgeSpec('Low-medium', 'warning', 'heroicon-m-exclamation-triangle'),
|
||||||
|
'medium' => new BadgeSpec('Medium', 'warning', 'heroicon-m-exclamation-triangle'),
|
||||||
|
'medium_high' => new BadgeSpec('Medium-high', 'danger', 'heroicon-m-x-circle'),
|
||||||
|
'high' => new BadgeSpec('High', 'danger', 'heroicon-m-x-circle'),
|
||||||
|
'n/a' => new BadgeSpec('N/A', 'gray', 'heroicon-m-minus-circle'),
|
||||||
|
default => BadgeSpec::unknown(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
21
app/Support/Badges/Domains/PolicySnapshotModeBadge.php
Normal file
21
app/Support/Badges/Domains/PolicySnapshotModeBadge.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support\Badges\Domains;
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeMapper;
|
||||||
|
use App\Support\Badges\BadgeSpec;
|
||||||
|
|
||||||
|
final class PolicySnapshotModeBadge implements BadgeMapper
|
||||||
|
{
|
||||||
|
public function spec(mixed $value): BadgeSpec
|
||||||
|
{
|
||||||
|
$state = BadgeCatalog::normalizeState($value);
|
||||||
|
|
||||||
|
return match ($state) {
|
||||||
|
'full' => new BadgeSpec('Full', 'success', 'heroicon-m-check-circle'),
|
||||||
|
'metadata_only' => new BadgeSpec('Metadata only', 'warning', 'heroicon-m-exclamation-triangle'),
|
||||||
|
default => BadgeSpec::unknown(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
22
app/Support/Badges/Domains/RestoreCheckSeverityBadge.php
Normal file
22
app/Support/Badges/Domains/RestoreCheckSeverityBadge.php
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support\Badges\Domains;
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeMapper;
|
||||||
|
use App\Support\Badges\BadgeSpec;
|
||||||
|
|
||||||
|
final class RestoreCheckSeverityBadge implements BadgeMapper
|
||||||
|
{
|
||||||
|
public function spec(mixed $value): BadgeSpec
|
||||||
|
{
|
||||||
|
$state = BadgeCatalog::normalizeState($value);
|
||||||
|
|
||||||
|
return match ($state) {
|
||||||
|
'blocking' => new BadgeSpec('Blocking', 'danger', 'heroicon-m-x-circle'),
|
||||||
|
'warning' => new BadgeSpec('Warning', 'warning', 'heroicon-m-exclamation-triangle'),
|
||||||
|
'safe' => new BadgeSpec('Safe', 'success', 'heroicon-m-check-circle'),
|
||||||
|
default => BadgeSpec::unknown(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
24
app/Support/Badges/Domains/RestorePreviewDecisionBadge.php
Normal file
24
app/Support/Badges/Domains/RestorePreviewDecisionBadge.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support\Badges\Domains;
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeMapper;
|
||||||
|
use App\Support\Badges\BadgeSpec;
|
||||||
|
|
||||||
|
final class RestorePreviewDecisionBadge implements BadgeMapper
|
||||||
|
{
|
||||||
|
public function spec(mixed $value): BadgeSpec
|
||||||
|
{
|
||||||
|
$state = BadgeCatalog::normalizeState($value);
|
||||||
|
|
||||||
|
return match ($state) {
|
||||||
|
'created' => new BadgeSpec('Created', 'success', 'heroicon-m-check-circle'),
|
||||||
|
'created_copy' => new BadgeSpec('Created copy', 'warning', 'heroicon-m-exclamation-triangle'),
|
||||||
|
'mapped_existing' => new BadgeSpec('Mapped existing', 'info', 'heroicon-m-arrow-path'),
|
||||||
|
'skipped' => new BadgeSpec('Skipped', 'warning', 'heroicon-m-minus-circle'),
|
||||||
|
'failed' => new BadgeSpec('Failed', 'danger', 'heroicon-m-x-circle'),
|
||||||
|
default => BadgeSpec::unknown(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
26
app/Support/Badges/Domains/RestoreResultStatusBadge.php
Normal file
26
app/Support/Badges/Domains/RestoreResultStatusBadge.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support\Badges\Domains;
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeMapper;
|
||||||
|
use App\Support\Badges\BadgeSpec;
|
||||||
|
|
||||||
|
final class RestoreResultStatusBadge implements BadgeMapper
|
||||||
|
{
|
||||||
|
public function spec(mixed $value): BadgeSpec
|
||||||
|
{
|
||||||
|
$state = BadgeCatalog::normalizeState($value);
|
||||||
|
|
||||||
|
return match ($state) {
|
||||||
|
'applied' => new BadgeSpec('Applied', 'success', 'heroicon-m-check-circle'),
|
||||||
|
'dry_run' => new BadgeSpec('Dry run', 'info', 'heroicon-m-arrow-path'),
|
||||||
|
'mapped' => new BadgeSpec('Mapped', 'success', 'heroicon-m-check-circle'),
|
||||||
|
'skipped' => new BadgeSpec('Skipped', 'warning', 'heroicon-m-minus-circle'),
|
||||||
|
'partial' => new BadgeSpec('Partial', 'warning', 'heroicon-m-exclamation-triangle'),
|
||||||
|
'manual_required' => new BadgeSpec('Manual required', 'warning', 'heroicon-m-exclamation-triangle'),
|
||||||
|
'failed' => new BadgeSpec('Failed', 'danger', 'heroicon-m-x-circle'),
|
||||||
|
default => BadgeSpec::unknown(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
33
app/Support/Badges/Domains/RestoreRunStatusBadge.php
Normal file
33
app/Support/Badges/Domains/RestoreRunStatusBadge.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support\Badges\Domains;
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeMapper;
|
||||||
|
use App\Support\Badges\BadgeSpec;
|
||||||
|
use App\Support\RestoreRunStatus;
|
||||||
|
|
||||||
|
final class RestoreRunStatusBadge implements BadgeMapper
|
||||||
|
{
|
||||||
|
public function spec(mixed $value): BadgeSpec
|
||||||
|
{
|
||||||
|
$state = BadgeCatalog::normalizeState($value);
|
||||||
|
|
||||||
|
return match ($state) {
|
||||||
|
RestoreRunStatus::Draft->value => new BadgeSpec('Draft', 'gray', 'heroicon-m-minus-circle'),
|
||||||
|
RestoreRunStatus::Scoped->value => new BadgeSpec('Scoped', 'gray', 'heroicon-m-minus-circle'),
|
||||||
|
RestoreRunStatus::Checked->value => new BadgeSpec('Checked', 'gray', 'heroicon-m-minus-circle'),
|
||||||
|
RestoreRunStatus::Previewed->value => new BadgeSpec('Previewed', 'gray', 'heroicon-m-minus-circle'),
|
||||||
|
RestoreRunStatus::Pending->value => new BadgeSpec('Pending', 'warning', 'heroicon-m-clock'),
|
||||||
|
RestoreRunStatus::Queued->value => new BadgeSpec('Queued', 'warning', 'heroicon-m-clock'),
|
||||||
|
RestoreRunStatus::Running->value => new BadgeSpec('Running', 'info', 'heroicon-m-arrow-path'),
|
||||||
|
RestoreRunStatus::Completed->value => new BadgeSpec('Completed', 'success', 'heroicon-m-check-circle'),
|
||||||
|
RestoreRunStatus::Partial->value => new BadgeSpec('Partial', 'warning', 'heroicon-m-exclamation-triangle'),
|
||||||
|
RestoreRunStatus::Failed->value => new BadgeSpec('Failed', 'danger', 'heroicon-m-x-circle'),
|
||||||
|
RestoreRunStatus::Cancelled->value => new BadgeSpec('Cancelled', 'gray', 'heroicon-m-minus-circle'),
|
||||||
|
RestoreRunStatus::Aborted->value => new BadgeSpec('Aborted', 'gray', 'heroicon-m-minus-circle'),
|
||||||
|
RestoreRunStatus::CompletedWithErrors->value => new BadgeSpec('Completed with errors', 'warning', 'heroicon-m-exclamation-triangle'),
|
||||||
|
default => BadgeSpec::unknown(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
24
app/Support/Badges/Domains/TenantAppStatusBadge.php
Normal file
24
app/Support/Badges/Domains/TenantAppStatusBadge.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support\Badges\Domains;
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeMapper;
|
||||||
|
use App\Support\Badges\BadgeSpec;
|
||||||
|
|
||||||
|
final class TenantAppStatusBadge implements BadgeMapper
|
||||||
|
{
|
||||||
|
public function spec(mixed $value): BadgeSpec
|
||||||
|
{
|
||||||
|
$state = BadgeCatalog::normalizeState($value);
|
||||||
|
|
||||||
|
return match ($state) {
|
||||||
|
'ok' => new BadgeSpec('OK', 'success', 'heroicon-m-check-circle'),
|
||||||
|
'configured' => new BadgeSpec('Configured', 'success', 'heroicon-m-check-circle'),
|
||||||
|
'pending' => new BadgeSpec('Pending', 'warning', 'heroicon-m-clock'),
|
||||||
|
'requires_consent', 'consent_required' => new BadgeSpec('Consent required', 'warning', 'heroicon-m-exclamation-triangle'),
|
||||||
|
'error' => new BadgeSpec('Error', 'danger', 'heroicon-m-x-circle'),
|
||||||
|
default => BadgeSpec::unknown(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
22
app/Support/Badges/Domains/TenantPermissionStatusBadge.php
Normal file
22
app/Support/Badges/Domains/TenantPermissionStatusBadge.php
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support\Badges\Domains;
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeMapper;
|
||||||
|
use App\Support\Badges\BadgeSpec;
|
||||||
|
|
||||||
|
final class TenantPermissionStatusBadge implements BadgeMapper
|
||||||
|
{
|
||||||
|
public function spec(mixed $value): BadgeSpec
|
||||||
|
{
|
||||||
|
$state = BadgeCatalog::normalizeState($value);
|
||||||
|
|
||||||
|
return match ($state) {
|
||||||
|
'granted' => new BadgeSpec('Granted', 'success', 'heroicon-m-check-circle'),
|
||||||
|
'missing' => new BadgeSpec('Missing', 'warning', 'heroicon-m-exclamation-triangle'),
|
||||||
|
'error' => new BadgeSpec('Error', 'danger', 'heroicon-m-x-circle'),
|
||||||
|
default => BadgeSpec::unknown(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
25
app/Support/Badges/Domains/TenantRbacStatusBadge.php
Normal file
25
app/Support/Badges/Domains/TenantRbacStatusBadge.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support\Badges\Domains;
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeMapper;
|
||||||
|
use App\Support\Badges\BadgeSpec;
|
||||||
|
|
||||||
|
final class TenantRbacStatusBadge implements BadgeMapper
|
||||||
|
{
|
||||||
|
public function spec(mixed $value): BadgeSpec
|
||||||
|
{
|
||||||
|
$state = BadgeCatalog::normalizeState($value);
|
||||||
|
|
||||||
|
return match ($state) {
|
||||||
|
'ok' => new BadgeSpec('OK', 'success', 'heroicon-m-check-circle'),
|
||||||
|
'configured' => new BadgeSpec('Configured', 'success', 'heroicon-m-check-circle'),
|
||||||
|
'manual_assignment_required' => new BadgeSpec('Manual assignment required', 'warning', 'heroicon-m-exclamation-triangle'),
|
||||||
|
'not_configured' => new BadgeSpec('Not configured', 'gray', 'heroicon-m-minus-circle'),
|
||||||
|
'error' => new BadgeSpec('Error', 'danger', 'heroicon-m-x-circle'),
|
||||||
|
'failed' => new BadgeSpec('Failed', 'danger', 'heroicon-m-x-circle'),
|
||||||
|
default => BadgeSpec::unknown(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
24
app/Support/Badges/Domains/TenantStatusBadge.php
Normal file
24
app/Support/Badges/Domains/TenantStatusBadge.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support\Badges\Domains;
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeMapper;
|
||||||
|
use App\Support\Badges\BadgeSpec;
|
||||||
|
|
||||||
|
final class TenantStatusBadge implements BadgeMapper
|
||||||
|
{
|
||||||
|
public function spec(mixed $value): BadgeSpec
|
||||||
|
{
|
||||||
|
$state = BadgeCatalog::normalizeState($value);
|
||||||
|
|
||||||
|
return match ($state) {
|
||||||
|
'active' => new BadgeSpec('Active', 'success', 'heroicon-m-check-circle'),
|
||||||
|
'inactive' => new BadgeSpec('Inactive', 'gray', 'heroicon-m-minus-circle'),
|
||||||
|
'archived' => new BadgeSpec('Archived', 'gray', 'heroicon-m-minus-circle'),
|
||||||
|
'suspended' => new BadgeSpec('Suspended', 'warning', 'heroicon-m-exclamation-triangle'),
|
||||||
|
'error' => new BadgeSpec('Error', 'danger', 'heroicon-m-x-circle'),
|
||||||
|
default => BadgeSpec::unknown(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -22,12 +22,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$severityColor = static function (?string $severity): string {
|
$severitySpec = static function (?string $severity): \App\Support\Badges\BadgeSpec {
|
||||||
return match ($severity) {
|
return \App\Support\Badges\BadgeRenderer::spec(\App\Support\Badges\BadgeDomain::RestoreCheckSeverity, $severity);
|
||||||
'blocking' => 'danger',
|
|
||||||
'warning' => 'warning',
|
|
||||||
default => 'success',
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$limitedList = static function (array $items, int $limit = 5): array {
|
$limitedList = static function (array $items, int $limit = 5): array {
|
||||||
@ -91,8 +87,12 @@
|
|||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<x-filament::badge :color="$severityColor($severity)" size="sm">
|
@php
|
||||||
{{ ucfirst((string) $severity) }}
|
$spec = $severitySpec($severity);
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<x-filament::badge :color="$spec->color" :icon="$spec->icon" size="sm">
|
||||||
|
{{ $spec->label }}
|
||||||
</x-filament::badge>
|
</x-filament::badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -18,21 +18,14 @@
|
|||||||
@foreach ($foundationItems as $item)
|
@foreach ($foundationItems as $item)
|
||||||
@php
|
@php
|
||||||
$decision = $item['decision'] ?? 'mapped_existing';
|
$decision = $item['decision'] ?? 'mapped_existing';
|
||||||
$decisionColor = match ($decision) {
|
$decisionSpec = \App\Support\Badges\BadgeRenderer::spec(\App\Support\Badges\BadgeDomain::RestorePreviewDecision, $decision);
|
||||||
'created' => 'text-green-700 bg-green-100 border-green-200',
|
|
||||||
'created_copy' => 'text-amber-900 bg-amber-100 border-amber-200',
|
|
||||||
'mapped_existing' => 'text-blue-700 bg-blue-100 border-blue-200',
|
|
||||||
'failed' => 'text-red-700 bg-red-100 border-red-200',
|
|
||||||
'skipped' => 'text-amber-900 bg-amber-50 border-amber-200',
|
|
||||||
default => 'text-gray-700 bg-gray-100 border-gray-200',
|
|
||||||
};
|
|
||||||
@endphp
|
@endphp
|
||||||
<div class="rounded border border-gray-200 bg-white p-3 shadow-sm">
|
<div class="rounded border border-gray-200 bg-white p-3 shadow-sm">
|
||||||
<div class="flex items-center justify-between text-sm text-gray-800">
|
<div class="flex items-center justify-between text-sm text-gray-800">
|
||||||
<span class="font-semibold">{{ $item['sourceName'] ?? $item['sourceId'] ?? 'Foundation' }}</span>
|
<span class="font-semibold">{{ $item['sourceName'] ?? $item['sourceId'] ?? 'Foundation' }}</span>
|
||||||
<span class="rounded border px-2 py-0.5 text-xs font-semibold uppercase tracking-wide {{ $decisionColor }}">
|
<x-filament::badge :color="$decisionSpec->color" :icon="$decisionSpec->icon" size="sm">
|
||||||
{{ $decision }}
|
{{ $decisionSpec->label }}
|
||||||
</span>
|
</x-filament::badge>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1 text-xs text-gray-600">
|
<div class="mt-1 text-xs text-gray-600">
|
||||||
{{ $item['type'] ?? 'foundation' }}
|
{{ $item['type'] ?? 'foundation' }}
|
||||||
@ -61,13 +54,16 @@
|
|||||||
@endphp
|
@endphp
|
||||||
<div class="rounded border border-gray-200 bg-white p-3 shadow-sm">
|
<div class="rounded border border-gray-200 bg-white p-3 shadow-sm">
|
||||||
<div class="flex items-center justify-between text-sm text-gray-800">
|
<div class="flex items-center justify-between text-sm text-gray-800">
|
||||||
<span class="font-semibold">{{ $item['policy_identifier'] ?? 'Policy' }}</span>
|
<span class="font-semibold">{{ $item['policy_identifier'] ?? 'Policy' }}</span>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@if ($restoreMode === 'preview-only')
|
@if ($restoreMode === 'preview-only')
|
||||||
<span class="rounded border border-amber-200 bg-amber-50 px-2 py-0.5 text-xs font-semibold uppercase tracking-wide text-amber-900">
|
@php
|
||||||
preview-only
|
$restoreModeSpec = \App\Support\Badges\BadgeRenderer::spec(\App\Support\Badges\BadgeDomain::PolicyRestoreMode, $restoreMode);
|
||||||
</span>
|
@endphp
|
||||||
@endif
|
<x-filament::badge :color="$restoreModeSpec->color" :icon="$restoreModeSpec->icon" size="sm">
|
||||||
|
{{ $restoreModeSpec->label }}
|
||||||
|
</x-filament::badge>
|
||||||
|
@endif
|
||||||
<span class="rounded bg-gray-100 px-2 py-0.5 text-xs uppercase tracking-wide text-gray-700">
|
<span class="rounded bg-gray-100 px-2 py-0.5 text-xs uppercase tracking-wide text-gray-700">
|
||||||
{{ $item['action'] ?? 'action' }}
|
{{ $item['action'] ?? 'action' }}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -53,21 +53,14 @@
|
|||||||
@foreach ($foundationItems as $item)
|
@foreach ($foundationItems as $item)
|
||||||
@php
|
@php
|
||||||
$decision = $item['decision'] ?? 'mapped_existing';
|
$decision = $item['decision'] ?? 'mapped_existing';
|
||||||
$decisionColor = match ($decision) {
|
$decisionSpec = \App\Support\Badges\BadgeRenderer::spec(\App\Support\Badges\BadgeDomain::RestorePreviewDecision, $decision);
|
||||||
'created' => 'text-green-700 bg-green-100 border-green-200',
|
|
||||||
'created_copy' => 'text-amber-900 bg-amber-100 border-amber-200',
|
|
||||||
'mapped_existing' => 'text-blue-700 bg-blue-100 border-blue-200',
|
|
||||||
'failed' => 'text-red-700 bg-red-100 border-red-200',
|
|
||||||
'skipped' => 'text-amber-900 bg-amber-50 border-amber-200',
|
|
||||||
default => 'text-gray-700 bg-gray-100 border-gray-200',
|
|
||||||
};
|
|
||||||
@endphp
|
@endphp
|
||||||
<div class="rounded border border-gray-200 bg-white p-3 shadow-sm">
|
<div class="rounded border border-gray-200 bg-white p-3 shadow-sm">
|
||||||
<div class="flex items-center justify-between text-sm text-gray-800">
|
<div class="flex items-center justify-between text-sm text-gray-800">
|
||||||
<span class="font-semibold">{{ $item['sourceName'] ?? $item['sourceId'] ?? 'Foundation' }}</span>
|
<span class="font-semibold">{{ $item['sourceName'] ?? $item['sourceId'] ?? 'Foundation' }}</span>
|
||||||
<span class="rounded border px-2 py-0.5 text-xs font-semibold uppercase tracking-wide {{ $decisionColor }}">
|
<x-filament::badge :color="$decisionSpec->color" :icon="$decisionSpec->icon" size="sm">
|
||||||
{{ $decision }}
|
{{ $decisionSpec->label }}
|
||||||
</span>
|
</x-filament::badge>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1 text-xs text-gray-600">
|
<div class="mt-1 text-xs text-gray-600">
|
||||||
{{ $item['type'] ?? 'foundation' }}
|
{{ $item['type'] ?? 'foundation' }}
|
||||||
@ -106,25 +99,20 @@
|
|||||||
@php
|
@php
|
||||||
$status = $item['status'] ?? 'unknown';
|
$status = $item['status'] ?? 'unknown';
|
||||||
$restoreMode = $item['restore_mode'] ?? null;
|
$restoreMode = $item['restore_mode'] ?? null;
|
||||||
$statusColor = match ($status) {
|
$statusSpec = \App\Support\Badges\BadgeRenderer::spec(\App\Support\Badges\BadgeDomain::RestoreResultStatus, $status);
|
||||||
'applied' => 'text-green-700 bg-green-100 border-green-200',
|
|
||||||
'dry_run' => 'text-blue-700 bg-blue-100 border-blue-200',
|
|
||||||
'skipped' => 'text-amber-900 bg-amber-50 border-amber-200',
|
|
||||||
'partial' => 'text-amber-900 bg-amber-50 border-amber-200',
|
|
||||||
'manual_required' => 'text-amber-900 bg-amber-100 border-amber-200',
|
|
||||||
'failed' => 'text-red-700 bg-red-100 border-red-200',
|
|
||||||
default => 'text-gray-700 bg-gray-100 border-gray-200',
|
|
||||||
};
|
|
||||||
@endphp
|
@endphp
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@if ($restoreMode === 'preview-only')
|
@if ($restoreMode === 'preview-only')
|
||||||
<span class="rounded border border-amber-200 bg-amber-50 px-2 py-0.5 text-xs font-semibold uppercase tracking-wide text-amber-900">
|
@php
|
||||||
preview-only
|
$restoreModeSpec = \App\Support\Badges\BadgeRenderer::spec(\App\Support\Badges\BadgeDomain::PolicyRestoreMode, $restoreMode);
|
||||||
</span>
|
@endphp
|
||||||
@endif
|
<x-filament::badge :color="$restoreModeSpec->color" :icon="$restoreModeSpec->icon" size="sm">
|
||||||
<span class="rounded border px-2 py-0.5 text-xs font-semibold uppercase tracking-wide {{ $statusColor }}">
|
{{ $restoreModeSpec->label }}
|
||||||
{{ $status }}
|
</x-filament::badge>
|
||||||
</span>
|
@endif
|
||||||
|
<x-filament::badge :color="$statusSpec->color" :icon="$statusSpec->icon" size="sm">
|
||||||
|
{{ $statusSpec->label }}
|
||||||
|
</x-filament::badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -165,11 +153,7 @@
|
|||||||
@foreach ($assignmentIssues as $outcome)
|
@foreach ($assignmentIssues as $outcome)
|
||||||
@php
|
@php
|
||||||
$outcomeStatus = $outcome['status'] ?? 'unknown';
|
$outcomeStatus = $outcome['status'] ?? 'unknown';
|
||||||
$outcomeColor = match ($outcomeStatus) {
|
$outcomeSpec = \App\Support\Badges\BadgeRenderer::spec(\App\Support\Badges\BadgeDomain::RestoreResultStatus, $outcomeStatus);
|
||||||
'failed' => 'text-red-700 bg-red-100 border-red-200',
|
|
||||||
'skipped' => 'text-amber-900 bg-amber-100 border-amber-200',
|
|
||||||
default => 'text-gray-700 bg-gray-100 border-gray-200',
|
|
||||||
};
|
|
||||||
$assignmentGroupId = $outcome['group_id']
|
$assignmentGroupId = $outcome['group_id']
|
||||||
?? ($outcome['assignment']['target']['groupId'] ?? null);
|
?? ($outcome['assignment']['target']['groupId'] ?? null);
|
||||||
$assignmentGroupLabel = $formatGroupId(is_string($assignmentGroupId) ? $assignmentGroupId : null);
|
$assignmentGroupLabel = $formatGroupId(is_string($assignmentGroupId) ? $assignmentGroupId : null);
|
||||||
@ -182,9 +166,9 @@
|
|||||||
<div class="font-semibold text-gray-900">
|
<div class="font-semibold text-gray-900">
|
||||||
Assignment {{ $assignmentGroupLabel ?? ($assignmentGroupId ?? 'unknown group') }}
|
Assignment {{ $assignmentGroupLabel ?? ($assignmentGroupId ?? 'unknown group') }}
|
||||||
</div>
|
</div>
|
||||||
<span class="rounded border px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide {{ $outcomeColor }}">
|
<x-filament::badge :color="$outcomeSpec->color" :icon="$outcomeSpec->icon" size="sm">
|
||||||
{{ $outcomeStatus }}
|
{{ $outcomeSpec->label }}
|
||||||
</span>
|
</x-filament::badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (! empty($outcome['mapped_group_id']))
|
@if (! empty($outcome['mapped_group_id']))
|
||||||
@ -240,20 +224,16 @@
|
|||||||
@foreach ($complianceEntries as $outcome)
|
@foreach ($complianceEntries as $outcome)
|
||||||
@php
|
@php
|
||||||
$outcomeStatus = $outcome['status'] ?? 'unknown';
|
$outcomeStatus = $outcome['status'] ?? 'unknown';
|
||||||
$outcomeColor = match ($outcomeStatus) {
|
$outcomeSpec = \App\Support\Badges\BadgeRenderer::spec(\App\Support\Badges\BadgeDomain::RestoreResultStatus, $outcomeStatus);
|
||||||
'mapped' => 'text-green-700 bg-green-100 border-green-200',
|
|
||||||
'skipped' => 'text-amber-900 bg-amber-100 border-amber-200',
|
|
||||||
default => 'text-gray-700 bg-gray-100 border-gray-200',
|
|
||||||
};
|
|
||||||
@endphp
|
@endphp
|
||||||
<div class="rounded border border-amber-200 bg-white p-2">
|
<div class="rounded border border-amber-200 bg-white p-2">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="font-semibold text-gray-900">
|
<div class="font-semibold text-gray-900">
|
||||||
Template {{ $outcome['template_id'] ?? 'unknown' }}
|
Template {{ $outcome['template_id'] ?? 'unknown' }}
|
||||||
</div>
|
</div>
|
||||||
<span class="rounded border px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide {{ $outcomeColor }}">
|
<x-filament::badge :color="$outcomeSpec->color" :icon="$outcomeSpec->icon" size="sm">
|
||||||
{{ $outcomeStatus }}
|
{{ $outcomeSpec->label }}
|
||||||
</span>
|
</x-filament::badge>
|
||||||
</div>
|
</div>
|
||||||
@if (! empty($outcome['rule_name']))
|
@if (! empty($outcome['rule_name']))
|
||||||
<div class="mt-1 text-[11px] text-gray-700">
|
<div class="mt-1 text-[11px] text-gray-700">
|
||||||
|
|||||||
36
specs/059-unified-badges/checklists/requirements.md
Normal file
36
specs/059-unified-badges/checklists/requirements.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# Specification Quality Checklist: Unified Badge System (Single Source of Truth) v1
|
||||||
|
|
||||||
|
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
||||||
|
**Created**: 2026-01-22
|
||||||
|
**Feature**: [spec.md](../spec.md)
|
||||||
|
|
||||||
|
## Content Quality
|
||||||
|
|
||||||
|
- [x] No implementation details (languages, frameworks, APIs)
|
||||||
|
- [x] Focused on user value and business needs
|
||||||
|
- [x] Written for non-technical stakeholders
|
||||||
|
- [x] All mandatory sections completed
|
||||||
|
|
||||||
|
## Requirement Completeness
|
||||||
|
|
||||||
|
- [x] No [NEEDS CLARIFICATION] markers remain
|
||||||
|
- [x] Requirements are testable and unambiguous
|
||||||
|
- [x] Success criteria are measurable
|
||||||
|
- [x] Success criteria are technology-agnostic (no implementation details)
|
||||||
|
- [x] All acceptance scenarios are defined
|
||||||
|
- [x] Edge cases are identified
|
||||||
|
- [x] Scope is clearly bounded
|
||||||
|
- [x] Dependencies and assumptions identified
|
||||||
|
|
||||||
|
## Feature Readiness
|
||||||
|
|
||||||
|
- [x] All functional requirements have clear acceptance criteria
|
||||||
|
- [x] User scenarios cover primary flows
|
||||||
|
- [x] Feature meets measurable outcomes defined in Success Criteria
|
||||||
|
- [x] No implementation details leak into specification
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Validation result: PASS
|
||||||
|
- Items marked incomplete require spec updates before `/speckit.clarify` or `/speckit.plan`
|
||||||
|
|
||||||
42
specs/059-unified-badges/contracts/badge-semantics.md
Normal file
42
specs/059-unified-badges/contracts/badge-semantics.md
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# Badge Semantics Contract — Unified Badge System (v1)
|
||||||
|
|
||||||
|
This feature does not introduce HTTP APIs. These contracts define stable UI semantics for status-like badges across the admin panel.
|
||||||
|
|
||||||
|
## Scope (v1)
|
||||||
|
|
||||||
|
### In scope (status-like)
|
||||||
|
- Status/health signals (for example: queued/running/completed; enabled/disabled; available/missing; acknowledged/new).
|
||||||
|
- Severity/risk signals (for example: drift finding severity).
|
||||||
|
|
||||||
|
### Out of scope (v1)
|
||||||
|
- Tag/category chips (for example: policy type, platform, environment labels).
|
||||||
|
|
||||||
|
## Canonical color meanings (v1)
|
||||||
|
|
||||||
|
- `success`: successful completion / safe / positive terminal meaning.
|
||||||
|
- `warning`: queued / needs attention / partial / caution.
|
||||||
|
- `info`: actively running / in progress.
|
||||||
|
- `danger`: failed / blocking / high risk.
|
||||||
|
- `gray`: neutral, unknown, or “not applicable”.
|
||||||
|
|
||||||
|
## Invariants (v1)
|
||||||
|
|
||||||
|
- Success/completed outcomes must never render as warning/attention.
|
||||||
|
- Unknown values must render safely (neutral) and must not be misrepresented as success.
|
||||||
|
- Badge rendering must be tenant-safe and must not trigger outbound HTTP, queued work, or DB lookups beyond already-loaded data.
|
||||||
|
|
||||||
|
## Canonical mappings (v1)
|
||||||
|
|
||||||
|
### Drift finding severity
|
||||||
|
- `low` → neutral (`gray`)
|
||||||
|
- `medium` → caution (`warning`)
|
||||||
|
- `high` → high risk (`danger`)
|
||||||
|
|
||||||
|
### Run-like states (minimum)
|
||||||
|
Run-like states must consistently convey:
|
||||||
|
- queued → caution (`warning`)
|
||||||
|
- running → in progress (`info`)
|
||||||
|
- succeeded/completed → success (`success`)
|
||||||
|
- partial → caution (`warning`)
|
||||||
|
- failed → failure (`danger`)
|
||||||
|
|
||||||
29
specs/059-unified-badges/contracts/guardrails.md
Normal file
29
specs/059-unified-badges/contracts/guardrails.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Guardrails Contract — Unified Badge System (v1)
|
||||||
|
|
||||||
|
This feature enforces a single source of truth for status-like badge semantics (status/health and severity/risk).
|
||||||
|
|
||||||
|
## What is enforced (v1)
|
||||||
|
|
||||||
|
- Status-like badges must not define meaning ad-hoc per page/resource/widget/view.
|
||||||
|
- Central semantics must be used for status/health and severity/risk signals.
|
||||||
|
|
||||||
|
Tag/category chips are out of scope for v1 and are not blocked by these guardrails.
|
||||||
|
|
||||||
|
## Guard strategy (v1)
|
||||||
|
|
||||||
|
### Mapping tests
|
||||||
|
- Each status-like badge domain has automated tests that assert canonical mappings and invariants.
|
||||||
|
- Invariant example: success/completed must never be represented with warning/attention meaning.
|
||||||
|
|
||||||
|
### Lightweight repo guard
|
||||||
|
- A test scans the codebase and flags newly introduced ad-hoc status-like badge semantics patterns.
|
||||||
|
- The guard is intentionally lightweight (pattern-based) to keep noise low while preventing regression.
|
||||||
|
|
||||||
|
## Examples of patterns the guard should flag
|
||||||
|
|
||||||
|
- Ad-hoc status/severity `match` blocks inside badge rendering logic for status-like fields.
|
||||||
|
- Per-resource status badge color maps defined inline on list pages/widgets.
|
||||||
|
- Custom Blade badge markup for status-like semantics when the central system exists.
|
||||||
|
|
||||||
|
The exact patterns are defined by the guard test implementation and may be refined to reduce false positives.
|
||||||
|
|
||||||
66
specs/059-unified-badges/data-model.md
Normal file
66
specs/059-unified-badges/data-model.md
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
# Data Model — Unified Badge System (Single Source of Truth) v1
|
||||||
|
|
||||||
|
This feature is UI-only. It introduces no database schema changes.
|
||||||
|
|
||||||
|
## Entities (existing)
|
||||||
|
|
||||||
|
### Tenant
|
||||||
|
- **Role**: scope boundary for all queries and UI rendering.
|
||||||
|
- **Status-like fields used**:
|
||||||
|
- `status` (for example, `active`, `archived`)
|
||||||
|
- `is_current` (boolean)
|
||||||
|
|
||||||
|
### OperationRun
|
||||||
|
- **Role**: canonical operations/run observability.
|
||||||
|
- **Status-like fields used**:
|
||||||
|
- `status` (`queued|running|completed`)
|
||||||
|
- `outcome` (for example, `pending|succeeded|partial|failed|cancelled|...`)
|
||||||
|
|
||||||
|
### InventorySyncRun
|
||||||
|
- **Role**: inventory synchronization history and last-sync indicators.
|
||||||
|
- **Status-like fields used**:
|
||||||
|
- `status` (`pending|running|success|partial|failed|skipped`)
|
||||||
|
|
||||||
|
### BackupScheduleRun
|
||||||
|
- **Role**: scheduled backup history and status indicators.
|
||||||
|
- **Status-like fields used**:
|
||||||
|
- `status` (`running|success|partial|failed|canceled|skipped`)
|
||||||
|
|
||||||
|
### EntraGroupSyncRun
|
||||||
|
- **Role**: Entra group cache synchronization history.
|
||||||
|
- **Status-like fields used**:
|
||||||
|
- `status` (`pending|running|succeeded|partial|failed`)
|
||||||
|
- `safety_stop_triggered` (boolean)
|
||||||
|
|
||||||
|
### RestoreRun
|
||||||
|
- **Role**: restore orchestration progress and outcomes.
|
||||||
|
- **Status-like fields used**:
|
||||||
|
- `status` (enum-like string; see `RestoreRunStatus`)
|
||||||
|
|
||||||
|
### Finding
|
||||||
|
- **Role**: drift/risk indicators and triage workflow.
|
||||||
|
- **Status-like fields used**:
|
||||||
|
- `status` (`new|acknowledged`)
|
||||||
|
- `severity` (`low|medium|high`)
|
||||||
|
|
||||||
|
## Entities (code-only)
|
||||||
|
|
||||||
|
### Badge Domain
|
||||||
|
- **Role**: a named namespace for status-like values that must render consistently (for example, “inventory sync run status”, “drift finding severity”).
|
||||||
|
- **Uniqueness**: domain name is unique within the application.
|
||||||
|
|
||||||
|
### Badge Definition
|
||||||
|
- **Role**: the central mapping for a domain’s values → badge meaning.
|
||||||
|
- **Behavior**:
|
||||||
|
- Must be pure (no DB queries, no HTTP, no side effects).
|
||||||
|
- Must provide a safe default for unknown values.
|
||||||
|
|
||||||
|
## State transitions (existing)
|
||||||
|
|
||||||
|
### RestoreRunStatus (selected)
|
||||||
|
The restore run lifecycle is defined in code and must not be changed by this feature:
|
||||||
|
- Pre-flight states: draft → scoped → checked → previewed
|
||||||
|
- Execution: pending/queued → running → completed/partial/failed/cancelled
|
||||||
|
|
||||||
|
This feature standardizes how these values render; it does not change transitions.
|
||||||
|
|
||||||
150
specs/059-unified-badges/plan.md
Normal file
150
specs/059-unified-badges/plan.md
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
# Implementation Plan: Unified Badge System (Single Source of Truth) v1
|
||||||
|
|
||||||
|
**Branch**: `059-unified-badges` | **Date**: 2026-01-22 | **Spec**: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/059-unified-badges/spec.md`
|
||||||
|
**Input**: Feature specification from `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/059-unified-badges/spec.md`
|
||||||
|
|
||||||
|
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/scripts/` for helper scripts.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
- Standardize status/health and severity/risk badge semantics suite-wide using a centralized mapping system.
|
||||||
|
- Migrate all status-like badge surfaces (tables, dashboards/KPIs, detail views) to use the central semantics.
|
||||||
|
- Keep tag/category chips (policy type/platform/environment) out of scope for v1 (planned follow-up).
|
||||||
|
- Add automated regression coverage: mapping tests + a lightweight guard that flags reintroduced ad-hoc mappings.
|
||||||
|
|
||||||
|
## Technical Context
|
||||||
|
|
||||||
|
<!--
|
||||||
|
ACTION REQUIRED: Replace the content in this section with the technical details
|
||||||
|
for the project. The structure here is presented in advisory capacity to guide
|
||||||
|
the iteration process.
|
||||||
|
-->
|
||||||
|
|
||||||
|
**Language/Version**: PHP 8.4.15 (Laravel 12)
|
||||||
|
**Primary Dependencies**: Filament v5 + Livewire v4
|
||||||
|
**Storage**: PostgreSQL
|
||||||
|
**Testing**: Pest v4 (PHPUnit v12 runtime via `php artisan test`)
|
||||||
|
**Target Platform**: Web application (Filament admin panel)
|
||||||
|
**Project Type**: Web (Laravel monolith)
|
||||||
|
**Performance Goals**: Badge mapping is constant-time; no added queries or N+1; typical list pages render <2s for normal tenant sizes.
|
||||||
|
**Constraints**: Tenant-scoped; status-like badge rendering must be DB-only and must not trigger outbound HTTP or job dispatch during render/polling/hydration.
|
||||||
|
**Scale/Scope**: Suite-wide migration for status/health and severity/risk badges across tables, dashboards/KPIs, and detail views.
|
||||||
|
|
||||||
|
## Constitution Check
|
||||||
|
|
||||||
|
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||||
|
|
||||||
|
- Inventory-first: clarify what is “last observed” vs snapshots/backups
|
||||||
|
- Read/write separation: any writes require preview + confirmation + audit + tests
|
||||||
|
- Graph contract path: Graph calls only via `GraphClientInterface` + `config/graph_contracts.php`
|
||||||
|
- Deterministic capabilities: capability derivation is testable (snapshot/golden tests)
|
||||||
|
- Tenant isolation: all reads/writes tenant-scoped; cross-tenant views are explicit and access-checked
|
||||||
|
- Run observability: long-running/remote/queued work creates/reuses `OperationRun`; start surfaces enqueue-only; Monitoring is DB-only; DB-only <2s actions may skip runs but security-relevant ones still audit-log
|
||||||
|
- Automation: queued/scheduled ops use locks + idempotency; handle 429/503 with backoff+jitter
|
||||||
|
- Data minimization: Inventory stores metadata + whitelisted meta; logs contain no secrets/tokens
|
||||||
|
|
||||||
|
Status: ✅ No constitution violations (UI semantics only; no new Graph calls; no new write behavior; badge mapping is pure and tenant-safe).
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
### Documentation (this feature)
|
||||||
|
|
||||||
|
```text
|
||||||
|
/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/059-unified-badges/
|
||||||
|
├── plan.md # This file (/speckit.plan command output)
|
||||||
|
├── research.md # Phase 0 output (/speckit.plan command)
|
||||||
|
├── data-model.md # Phase 1 output (/speckit.plan command)
|
||||||
|
├── quickstart.md # Phase 1 output (/speckit.plan command)
|
||||||
|
├── contracts/ # Phase 1 output (/speckit.plan command)
|
||||||
|
└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Source Code (repository root)
|
||||||
|
|
||||||
|
```text
|
||||||
|
/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/
|
||||||
|
├── Filament/
|
||||||
|
│ ├── Pages/Monitoring/Operations.php # Update: migrate status/outcome badges to central mapping
|
||||||
|
│ ├── Resources/ # Update: status-like columns/entries across resources
|
||||||
|
│ └── Widgets/ # Update: status-like + severity badges in dashboard widgets
|
||||||
|
├── Support/
|
||||||
|
│ └── Badges/ # New: central badge semantics (status/health + severity/risk)
|
||||||
|
└── Models/ # Existing: status/severity sources (OperationRun, Finding, etc.)
|
||||||
|
|
||||||
|
/Users/ahmeddarrazi/Documents/projects/TenantAtlas/resources/views/
|
||||||
|
└── filament/ # Update: replace any ad-hoc status-like badge colors
|
||||||
|
|
||||||
|
/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/
|
||||||
|
├── Feature/Guards/ # New: lightweight “no ad-hoc badge semantics” guard
|
||||||
|
└── Unit/ # New/updated: badge mapping tests per domain
|
||||||
|
```
|
||||||
|
|
||||||
|
**Structure Decision**: Laravel monolith + Filament v5 conventions. Centralize semantics in `app/Support/Badges` and consume from Filament resources/pages/widgets + Blade views.
|
||||||
|
|
||||||
|
## Complexity Tracking
|
||||||
|
|
||||||
|
> **Fill ONLY if Constitution Check has violations that must be justified**
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Phase 0 — Outline & Research (complete)
|
||||||
|
|
||||||
|
- Output: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/059-unified-badges/research.md`
|
||||||
|
- Key decisions captured:
|
||||||
|
- v1 scope: status/health + severity/risk badges suite-wide; tag/category chips deferred.
|
||||||
|
- Drift severity mapping: low = neutral, medium = warning, high = danger.
|
||||||
|
- Enforcement: mapping tests + lightweight guard to prevent reintroducing ad-hoc mappings.
|
||||||
|
|
||||||
|
## Phase 1 — Design & Contracts (complete)
|
||||||
|
|
||||||
|
### Data model
|
||||||
|
- Output: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/059-unified-badges/data-model.md`
|
||||||
|
- No schema changes required; badge semantics derive from existing fields (status/outcome/severity booleans).
|
||||||
|
|
||||||
|
### Contracts
|
||||||
|
- Output: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/059-unified-badges/contracts/badge-semantics.md`
|
||||||
|
- Output: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/059-unified-badges/contracts/guardrails.md`
|
||||||
|
|
||||||
|
### Quickstart
|
||||||
|
- Output: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/059-unified-badges/quickstart.md`
|
||||||
|
|
||||||
|
### Provider registration (Laravel 11+)
|
||||||
|
- Panel providers remain registered in `bootstrap/providers.php` (no changes required for this feature unless adding a new provider).
|
||||||
|
|
||||||
|
### Livewire / Filament version safety
|
||||||
|
- Livewire v4.0+ (required by Filament v5) is in use.
|
||||||
|
|
||||||
|
### Asset strategy
|
||||||
|
- No new assets expected. If new panel assets are introduced during implementation, ensure deployment runs `php artisan filament:assets`.
|
||||||
|
|
||||||
|
### Destructive actions
|
||||||
|
- None introduced in this feature.
|
||||||
|
|
||||||
|
### Constitution re-check (post-design)
|
||||||
|
- ✅ Inventory-first / Snapshots-second: unaffected (UI-only semantics).
|
||||||
|
- ✅ Read/write separation: this feature is read-only.
|
||||||
|
- ✅ Graph contract path: no Graph calls added.
|
||||||
|
- ✅ Tenant isolation: badge mapping is pure and uses already-available tenant-scoped data.
|
||||||
|
- ✅ Run observability: only consumes existing run records; does not introduce new long-running work.
|
||||||
|
- ✅ Data minimization: no new payload storage.
|
||||||
|
|
||||||
|
**Gate status (post-design)**: PASS
|
||||||
|
|
||||||
|
## Phase 2 — Implementation Plan (next)
|
||||||
|
|
||||||
|
### Story 1 (P1): Trustworthy status/health badges everywhere
|
||||||
|
- Introduce a central badge semantics layer for status/health domains (runs, findings status, tenant status, booleans, availability).
|
||||||
|
- Migrate all status-like badge surfaces suite-wide to the centralized mapping, prioritizing:
|
||||||
|
- Monitoring/Operations list surfaces
|
||||||
|
- Inventory sync runs and backup schedule runs
|
||||||
|
- Restore runs
|
||||||
|
- Findings status
|
||||||
|
- Ensure the invariant: success/completed is never presented as warning/attention.
|
||||||
|
|
||||||
|
### Story 2 (P2): Readable status badges in dark mode
|
||||||
|
- Remove fragile per-page color overrides for status-like badges in Blade where present.
|
||||||
|
- Ensure status-like badges remain readable in dark mode and icons do not appear disabled unless intentionally neutral.
|
||||||
|
|
||||||
|
### Story 3 (P3): Consistency stays enforced over time
|
||||||
|
- Add mapping tests per domain (including drift severity mapping and “success is never warning” invariants).
|
||||||
|
- Add a lightweight guard test to detect newly introduced ad-hoc status/health or severity/risk badge mappings.
|
||||||
29
specs/059-unified-badges/quickstart.md
Normal file
29
specs/059-unified-badges/quickstart.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Quickstart — Unified Badge System (v1)
|
||||||
|
|
||||||
|
## Prereqs
|
||||||
|
- Run everything via Sail.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
- `vendor/bin/sail up -d`
|
||||||
|
- `vendor/bin/sail composer install`
|
||||||
|
|
||||||
|
## Run tests (targeted)
|
||||||
|
Existing safety nets to keep green:
|
||||||
|
- `vendor/bin/sail artisan test tests/Feature/Monitoring/OperationsDbOnlyTest.php`
|
||||||
|
- `vendor/bin/sail artisan test tests/Feature/Monitoring/OperationsTenantScopeTest.php`
|
||||||
|
|
||||||
|
When the feature is implemented, add + run:
|
||||||
|
- Badge mapping tests (new).
|
||||||
|
- Status-like ad-hoc mapping guard test (new).
|
||||||
|
|
||||||
|
## Manual QA (tenant-scoped)
|
||||||
|
- Operations/Monitoring: queued/running/completed + outcome badges are consistent and success is never warning.
|
||||||
|
- Drift findings: severity mapping is consistent (low=neutral, medium=warning, high=danger).
|
||||||
|
- Restore runs: all lifecycle statuses render consistently across list + detail.
|
||||||
|
- Dark mode: status-like badges remain readable; icons don’t appear disabled unless intentionally neutral.
|
||||||
|
|
||||||
|
## Frontend assets
|
||||||
|
If UI changes don’t show:
|
||||||
|
- `vendor/bin/sail npm run dev`
|
||||||
|
- or `vendor/bin/sail npm run build`
|
||||||
|
|
||||||
62
specs/059-unified-badges/research.md
Normal file
62
specs/059-unified-badges/research.md
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
# Research — Unified Badge System (Single Source of Truth) v1
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Standardize status/health and severity/risk badge semantics suite-wide so operators can reliably scan the admin UI without misread signals (for example, “success” never appearing as warning).
|
||||||
|
|
||||||
|
V1 scope explicitly excludes tag/category chips (policy type/platform/environment).
|
||||||
|
|
||||||
|
## Existing Code & Patterns (to reuse)
|
||||||
|
|
||||||
|
### Filament badge surfaces (current)
|
||||||
|
- Tables already use `TextColumn::badge()` in many places.
|
||||||
|
- Ad-hoc status and severity mapping exists in several hotspots, for example:
|
||||||
|
- `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Pages/Monitoring/Operations.php`
|
||||||
|
- `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Widgets/Dashboard/RecentDriftFindings.php`
|
||||||
|
- `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/resources/views/filament/forms/components/restore-run-checks.blade.php`
|
||||||
|
|
||||||
|
### Guard test pattern (current)
|
||||||
|
- The repo already uses Pest “guard” tests that scan the codebase for forbidden patterns:
|
||||||
|
- `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Feature/Guards/NoLegacyBulkOperationsTest.php`
|
||||||
|
|
||||||
|
### Status / severity sources (current)
|
||||||
|
Status-like values already exist in models/enums and must remain the source of truth for meaning:
|
||||||
|
- Operation runs: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Models/OperationRun.php` (status) + outcome usage in UI.
|
||||||
|
- Inventory sync runs: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Models/InventorySyncRun.php` (status constants).
|
||||||
|
- Backup schedule runs: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Models/BackupScheduleRun.php` (status constants).
|
||||||
|
- Entra group sync runs: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Models/EntraGroupSyncRun.php` (status constants).
|
||||||
|
- Restore runs: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/RestoreRunStatus.php` (enum).
|
||||||
|
- Findings: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Models/Finding.php` (severity + status constants).
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
|
||||||
|
### Decision: Centralize status-like badge semantics behind domain mappers
|
||||||
|
- **Decision**: Introduce a central badge semantics layer that maps a “domain” + “value” to a single badge meaning (label + color meaning + optional icon).
|
||||||
|
- **Rationale**: Eliminates drift from per-page match blocks and keeps UI semantics testable and reviewable.
|
||||||
|
- **Alternatives considered**:
|
||||||
|
- **Inline mappings per resource/widget**: rejected (drifts quickly; hard to enforce).
|
||||||
|
- **Config-only mappings**: rejected for v1 (harder to type-check; still needs a rendering abstraction).
|
||||||
|
- **DB-stored mappings**: rejected (adds runtime dependency and migration/tenant complexity for no user value).
|
||||||
|
|
||||||
|
### Decision: V1 scope is “status-like” only (status/health + severity/risk)
|
||||||
|
- **Decision**: V1 migrates status-like badges suite-wide; tag/category chips are deferred.
|
||||||
|
- **Rationale**: Status/health and severity/risk are the highest-risk trust killers when inconsistent; tags are valuable but less safety-critical and more domain-specific.
|
||||||
|
|
||||||
|
### Decision: Canonical drift severity meanings
|
||||||
|
- **Decision**: Drift finding severity mapping is canonical: low = neutral, medium = warning, high = danger.
|
||||||
|
- **Rationale**: Severity is a risk/attention signal; “low” should not appear as “success”.
|
||||||
|
|
||||||
|
### Decision: No severity taxonomy changes in v1
|
||||||
|
- **Decision**: Do not add/rename severity levels (for example, do not introduce “critical” in v1).
|
||||||
|
- **Rationale**: This feature standardizes rendering semantics; changing underlying severity taxonomy is a separate scope and needs domain review.
|
||||||
|
|
||||||
|
### Decision: Enforcement is tests + a lightweight guard
|
||||||
|
- **Decision**: Add:
|
||||||
|
- Mapping tests per domain (including invariants like “success is never warning”).
|
||||||
|
- A lightweight guard test that flags newly introduced ad-hoc mappings for status/health and severity/risk.
|
||||||
|
- **Rationale**: Mapping tests prove correctness; the guard prevents regressions and enforces the single-source-of-truth rule.
|
||||||
|
- **Alternatives considered**:
|
||||||
|
- **Strict guard banning any badge usage not from the central system**: rejected (too brittle; would block deferred tag/category chip work and legitimate non-status uses).
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
None — remaining work is implementation-time discovery of all status-like badge surfaces to migrate.
|
||||||
|
|
||||||
151
specs/059-unified-badges/spec.md
Normal file
151
specs/059-unified-badges/spec.md
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
# Feature Specification: Unified Badge System (Single Source of Truth) v1
|
||||||
|
|
||||||
|
**Feature Branch**: `059-unified-badges`
|
||||||
|
**Created**: 2026-01-22
|
||||||
|
**Status**: Draft
|
||||||
|
**Input**: Suite-wide badge/chip standardization so the same underlying value always renders with the same meaning (label + color + optional icon) across tables, dashboards/KPIs, and detail views; tenant-safe and DB-only at render time where required.
|
||||||
|
|
||||||
|
## Clarifications
|
||||||
|
|
||||||
|
### Session 2026-01-22
|
||||||
|
|
||||||
|
- Q: What is the v1 migration coverage target? → A: Status-like badges suite-wide; tag/category chips later.
|
||||||
|
- Q: What counts as “status-like” for v1 scope? → A: Status/health plus severity/risk signals.
|
||||||
|
- Q: Should v1 introduce any new severity levels (e.g., “critical”), or standardize the existing severity values only? → A: Standardize existing severity values only (no new levels in v1).
|
||||||
|
- Q: Should v1 include an automated “no ad-hoc badge semantics” guard beyond mapping tests? → A: Yes — tests plus a lightweight automated guard that flags ad-hoc mappings.
|
||||||
|
- Q: What is the canonical meaning for drift finding severity (low | medium | high) in v1? → A: low = neutral, medium = warning, high = danger.
|
||||||
|
|
||||||
|
## User Scenarios & Testing *(mandatory)*
|
||||||
|
|
||||||
|
<!--
|
||||||
|
IMPORTANT: User stories should be PRIORITIZED as user journeys ordered by importance.
|
||||||
|
Each user story/journey must be INDEPENDENTLY TESTABLE - meaning if you implement just ONE of them,
|
||||||
|
you should still have a viable MVP (Minimum Viable Product) that delivers value.
|
||||||
|
|
||||||
|
Assign priorities (P1, P2, P3, etc.) to each story, where P1 is the most critical.
|
||||||
|
Think of each story as a standalone slice of functionality that can be:
|
||||||
|
- Developed independently
|
||||||
|
- Tested independently
|
||||||
|
- Deployed independently
|
||||||
|
- Demonstrated to users independently
|
||||||
|
-->
|
||||||
|
|
||||||
|
### User Story 1 - Trustworthy status badges everywhere (Priority: P1)
|
||||||
|
|
||||||
|
As a tenant admin, I can trust that status/health and severity/risk badges mean the same thing everywhere in the admin panel, so I can scan lists quickly and make the right decision.
|
||||||
|
|
||||||
|
**Why this priority**: Inconsistent badge semantics create operational mistakes (false “success”, missed failures) and erode trust in the UI.
|
||||||
|
|
||||||
|
**Independent Test**: View a representative set of pages that contain status/health and severity/risk badges (lists + detail views) and confirm the same underlying value always uses the same label and visual meaning across pages.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a run is in the “queued” state, **When** I view it in any table, dashboard list, or detail view, **Then** it is clearly shown as “Queued” with a consistent visual meaning that indicates “waiting to start”.
|
||||||
|
2. **Given** a run is in the “running” state, **When** I view it in any table, dashboard list, or detail view, **Then** it is clearly shown as “Running” with a consistent visual meaning that indicates “in progress”.
|
||||||
|
3. **Given** a run is in a successful terminal state (“succeeded” / “completed”), **When** I view it anywhere, **Then** it is shown with a consistent “success” meaning and is never shown using warning/attention colors.
|
||||||
|
4. **Given** a finding has a high-severity value, **When** I view it in any table, dashboard list, or detail view, **Then** it is shown with a consistent “high severity” meaning and is never shown as a neutral or low-attention meaning.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 2 - Readable status badges in dark mode (Priority: P2)
|
||||||
|
|
||||||
|
As a tenant admin, I can scan status badges in both light and dark mode without readability regressions.
|
||||||
|
|
||||||
|
**Why this priority**: Badges are a high-density UI element; readability and correct “good/bad” signaling reduce cognitive load and prevent mistakes.
|
||||||
|
|
||||||
|
**Independent Test**: Open key list pages and dashboards in dark mode and light mode and verify status badges remain readable without relying on fragile per-page styling overrides.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** I use dark mode, **When** I view status-like badges on common pages, **Then** badge text and any icons remain readable and do not rely on fragile per-page styling overrides.
|
||||||
|
2. **Given** a status badge includes an icon in a dense list, **When** I view it, **Then** the icon appearance matches the badge meaning and does not appear disabled unless the status is intentionally neutral.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 3 - Consistency stays enforced over time (Priority: P3)
|
||||||
|
|
||||||
|
As a maintainer, I can update badge semantics in one place and have the change apply everywhere, and regressions are caught before release.
|
||||||
|
|
||||||
|
**Why this priority**: Without enforcement, ad-hoc badge mappings quickly reappear and the UI drifts back into inconsistent meanings.
|
||||||
|
|
||||||
|
**Independent Test**: Make a small change to a centralized badge definition and confirm it affects multiple UI surfaces; introduce a deliberately inconsistent mapping and confirm automated validation fails.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a new status value is introduced, **When** it is not yet defined in the central badge system, **Then** it displays with a safe “unknown” meaning rather than being misrepresented as success or warning.
|
||||||
|
2. **Given** a developer attempts to reintroduce an ad-hoc badge mapping, **When** automated validation runs, **Then** it is detected and fails until the centralized definition is used.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[Add more user stories as needed, each with an assigned priority]
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
|
||||||
|
- A record has an unrecognized/legacy status value (null/empty/unknown string).
|
||||||
|
- A record has an unrecognized/legacy severity/risk value.
|
||||||
|
- The same status appears on multiple pages (list + detail) and must remain consistent.
|
||||||
|
- A status value exists across multiple “domains” (e.g., “completed” used in different workflows) and must not be conflated if meanings differ.
|
||||||
|
- Dark mode and high-contrast settings reduce readability of badge text or icons.
|
||||||
|
- A page that must remain read-only/DB-only accidentally introduces side effects during render (for example, remote calls or background work).
|
||||||
|
- Tenant switching occurs mid-session and badges must not leak cross-tenant data.
|
||||||
|
|
||||||
|
## Requirements *(mandatory)*
|
||||||
|
|
||||||
|
**Constitution alignment (required):** If this feature introduces any Microsoft Graph calls, any write/change behavior,
|
||||||
|
or any long-running/queued/scheduled work, the spec MUST describe contract registry updates, safety gates
|
||||||
|
(preview/confirmation/audit), tenant isolation, run observability (`OperationRun` type/identity/visibility), and tests.
|
||||||
|
If security-relevant DB-only actions intentionally skip `OperationRun`, the spec MUST describe `AuditLog` entries.
|
||||||
|
|
||||||
|
<!--
|
||||||
|
ACTION REQUIRED: The content in this section represents placeholders.
|
||||||
|
Fill them out with the right functional requirements.
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Functional Requirements
|
||||||
|
|
||||||
|
- **FR-001**: The system MUST define badge semantics centrally for each status badge “domain” used in the admin UI, including: label, color meaning, and (when applicable) an icon.
|
||||||
|
- **FR-002**: The system MUST apply the centralized status badge semantics consistently across all status badge surfaces (tables, dashboards/KPIs, and detail views) so the same underlying value always renders with the same meaning.
|
||||||
|
- **FR-003**: The system MUST clearly distinguish “status-like” badges (status/health and severity/risk signals) from tag/category chips so scope boundaries are unambiguous; v1 MUST standardize status-like badges suite-wide.
|
||||||
|
- **FR-004**: The system MUST standardize the canonical meanings for run-like statuses at minimum: queued, running, succeeded/completed, partial, failed.
|
||||||
|
- **FR-005**: Warning/attention colors (e.g., orange/yellow) MUST be reserved for “queued / needs attention / partial / in progress” meanings and MUST NOT represent success/completed outcomes.
|
||||||
|
- **FR-006**: Badge rendering MUST remain tenant-safe: it must not display cross-tenant data and must rely only on data already available in the current tenant context.
|
||||||
|
- **FR-007**: For designated DB-only pages (for example, Monitoring/Operations views), badge rendering MUST NOT trigger outbound network requests, background jobs, or other side effects during render or during automatic refresh/polling.
|
||||||
|
- **FR-008**: Badge rendering MUST be performant and predictable: it must not require additional data lookups at view time and must not introduce noticeable delays on high-row-count tables.
|
||||||
|
- **FR-009**: In dense tables, badges MAY include icons for scanability; when icons are shown, they MUST not appear disabled/gray unless the badge itself is intentionally neutral.
|
||||||
|
- **FR-010**: The system MUST provide a safe default for unrecognized values (neutral + clearly labeled as unknown) to avoid misleading operators.
|
||||||
|
- **FR-011**: The admin UI MUST be migrated so existing status-like badges/chips use the centralized system across the suite; tag/category chips are explicitly out of scope for v1 migration and may remain unchanged.
|
||||||
|
- **FR-012**: The delivery MUST include automated regression checks that validate canonical mappings (including “success is never warning”) and prevent reintroducing ad-hoc badge semantics.
|
||||||
|
- **FR-013**: This change MUST be limited to badge/chip rendering semantics; it MUST NOT change underlying workflow logic, status definitions, or page layouts beyond what is required to standardize badge rendering.
|
||||||
|
- **FR-014**: Severity/risk badges (for example, findings severity) MUST be standardized and rendered consistently across all in-scope pages.
|
||||||
|
- **FR-015**: The system MUST NOT introduce new severity levels as part of this feature; it MUST standardize and render existing severity values consistently.
|
||||||
|
- **FR-016**: The delivery MUST include a lightweight automated guard that detects newly introduced ad-hoc status/health or severity/risk badge semantics and blocks release until the centralized system is used.
|
||||||
|
- **FR-017**: Drift finding severity MUST have a canonical meaning: low = neutral, medium = warning, high = danger.
|
||||||
|
|
||||||
|
### Assumptions & Dependencies
|
||||||
|
|
||||||
|
- Existing status values and business meanings are already established; this feature standardizes how they are presented, not what they mean.
|
||||||
|
- A defined set of status-like badge domains exists across the suite (runs, findings status, tenant status, availability, enabled/disabled, severity/risk); any newly discovered status-like domains will be included in the v1 standardization scope.
|
||||||
|
- Dark mode is supported and is considered in acceptance for badge readability.
|
||||||
|
- Tag/category chip standardization (policy type/platform/environment) is deferred to a later version.
|
||||||
|
- Severity level changes (such as adding “critical”) are deferred to a later version.
|
||||||
|
|
||||||
|
### Key Entities *(include if feature involves data)*
|
||||||
|
|
||||||
|
- **Badge Domain**: A named category of values that share a consistent badge meaning (for example, “Operation run status”).
|
||||||
|
- **Badge Definition**: The centralized mapping for a domain’s values to label + color meaning + optional icon.
|
||||||
|
- **Status Badge**: A badge that communicates progress/outcome/health or severity/risk (for example, queued/running/succeeded).
|
||||||
|
- **Tag Badge**: A badge that communicates categorization/metadata (for example, platform/type/environment).
|
||||||
|
|
||||||
|
## Success Criteria *(mandatory)*
|
||||||
|
|
||||||
|
<!--
|
||||||
|
ACTION REQUIRED: Define measurable success criteria.
|
||||||
|
These must be technology-agnostic and measurable.
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Measurable Outcomes
|
||||||
|
|
||||||
|
- **SC-001**: For each defined status badge domain, the same value renders with the same label and visual meaning across all in-scope pages in 100% of validation runs.
|
||||||
|
- **SC-002**: Across the in-scope admin UI, 0 instances exist where a success/completed outcome is presented using a warning/attention badge meaning.
|
||||||
|
- **SC-003**: Viewing designated DB-only pages triggers 0 outbound network requests and 0 background work as a side effect of badge rendering, in 100% of regression runs.
|
||||||
|
- **SC-004**: Status-badge-related UI regressions (incorrect label/color/icon meaning) decrease by at least 80% in the 30 days after release compared to the previous 30 days.
|
||||||
176
specs/059-unified-badges/tasks.md
Normal file
176
specs/059-unified-badges/tasks.md
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
---
|
||||||
|
description: "Task list for feature implementation"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Tasks: Unified Badge System (Single Source of Truth) v1
|
||||||
|
|
||||||
|
**Input**: Design documents from `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/059-unified-badges/`
|
||||||
|
|
||||||
|
**Tests**: Required (Pest) — this feature changes runtime UI semantics and adds regression guardrails.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Setup (Shared Infrastructure)
|
||||||
|
|
||||||
|
- [X] T001 Confirm feature inputs exist: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/059-unified-badges/spec.md`, `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/059-unified-badges/plan.md`
|
||||||
|
- [X] T002 Confirm Phase 0/1 artifacts exist: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/059-unified-badges/research.md`, `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/059-unified-badges/data-model.md`, `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/059-unified-badges/contracts/badge-semantics.md`, `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/059-unified-badges/contracts/guardrails.md`, `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/059-unified-badges/quickstart.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Foundational (Blocking Prerequisites)
|
||||||
|
|
||||||
|
**Purpose**: Establish the centralized badge semantics layer that all user stories depend on.
|
||||||
|
|
||||||
|
- [X] T003 Create badge value object in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/BadgeSpec.php`
|
||||||
|
- [X] T004 Create badge domain + mapper contracts in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/BadgeDomain.php` and `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/BadgeMapper.php`
|
||||||
|
- [X] T005 Create central resolver/registry in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/BadgeCatalog.php` (safe unknown fallback; no side effects)
|
||||||
|
- [X] T006 Create Filament + Blade helper closures in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/BadgeRenderer.php` (table/infolist helpers for status-like badges)
|
||||||
|
- [X] T007 Add foundational unit coverage for unknown fallback + allowed color set in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Unit/Badges/BadgeCatalogTest.php`
|
||||||
|
|
||||||
|
**Checkpoint**: Central badge semantics infrastructure exists and is covered.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: User Story 1 — Trustworthy status/health + severity/risk badges everywhere (Priority: P1) 🎯 MVP
|
||||||
|
|
||||||
|
**Goal**: Status-like values (status/health and severity/risk) render consistently across the suite, using central semantics.
|
||||||
|
|
||||||
|
**Independent Test**: Run badge mapper tests and verify key pages (Operations + Drift findings + Restore runs) show consistent meanings, including “success is never warning”.
|
||||||
|
|
||||||
|
### Tests (US1)
|
||||||
|
|
||||||
|
- [X] T008 [P] [US1] Add OperationRun badge mapping tests in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Unit/Badges/OperationRunBadgesTest.php`
|
||||||
|
- [X] T009 [P] [US1] Add Finding status + severity mapping tests in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Unit/Badges/FindingBadgesTest.php`
|
||||||
|
- [X] T010 [P] [US1] Add RestoreRun status mapping tests in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Unit/Badges/RestoreRunBadgesTest.php`
|
||||||
|
- [X] T011 [P] [US1] Add InventorySyncRun + BackupScheduleRun mapping tests in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Unit/Badges/RunStatusBadgesTest.php`
|
||||||
|
|
||||||
|
### Implementation (US1)
|
||||||
|
|
||||||
|
- [X] T012 [US1] Implement OperationRun status/outcome badge domains in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/Domains/OperationRunStatusBadge.php` and `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/Domains/OperationRunOutcomeBadge.php`
|
||||||
|
- [X] T013 [US1] Implement drift finding severity badge domain in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/Domains/FindingSeverityBadge.php` (low=neutral, medium=warning, high=danger)
|
||||||
|
- [X] T014 [US1] Implement finding status badge domain in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/Domains/FindingStatusBadge.php`
|
||||||
|
- [X] T015 [US1] Implement RestoreRun status badge domain in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/Domains/RestoreRunStatusBadge.php`
|
||||||
|
- [X] T016 [US1] Implement InventorySyncRun status badge domain in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/Domains/InventorySyncRunStatusBadge.php`
|
||||||
|
- [X] T017 [US1] Implement BackupScheduleRun status badge domain in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/Domains/BackupScheduleRunStatusBadge.php`
|
||||||
|
- [X] T018 [US1] Implement EntraGroupSyncRun status badge domain in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/Domains/EntraGroupSyncRunStatusBadge.php`
|
||||||
|
- [X] T019 [US1] Implement status-like boolean badge domains in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/Domains/BooleanEnabledBadge.php` and `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/Domains/BooleanHasErrorsBadge.php`
|
||||||
|
|
||||||
|
### Migration (US1)
|
||||||
|
|
||||||
|
- [X] T020 [P] [US1] Migrate Operations resource badges to central mapping in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Resources/OperationRunResource.php` (remove ad-hoc `statusColor()` / `outcomeColor()` logic)
|
||||||
|
- [X] T021 [P] [US1] Migrate Monitoring Operations table badges to central mapping in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Pages/Monitoring/Operations.php` (remove `->colors([...])`)
|
||||||
|
- [X] T022 [P] [US1] Migrate dashboard “Recent Operations” badges to central mapping in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Widgets/Dashboard/RecentOperations.php`
|
||||||
|
- [X] T023 [P] [US1] Migrate dashboard “Recent Drift Findings” severity/status badges to central mapping in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Widgets/Dashboard/RecentDriftFindings.php`
|
||||||
|
- [X] T024 [P] [US1] Migrate Finding resource status/severity badges to central mapping in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Resources/FindingResource.php`
|
||||||
|
- [X] T025 [P] [US1] Migrate Inventory sync run status + had_errors badges to central mapping in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Resources/InventorySyncRunResource.php`
|
||||||
|
- [X] T026 [P] [US1] Migrate backup schedule “last run status” and runs relation manager badges to central mapping in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Resources/BackupScheduleResource.php` and `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Resources/BackupScheduleResource/RelationManagers/BackupScheduleRunsRelationManager.php` (leave tag-like frequency badge unchanged in v1)
|
||||||
|
- [X] T027 [P] [US1] Migrate Entra group sync run status badges to central mapping in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Resources/EntraGroupSyncRunResource.php`
|
||||||
|
- [X] T028 [P] [US1] Migrate Restore run status badges to central mapping in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Resources/RestoreRunResource.php`
|
||||||
|
- [X] T029 [P] [US1] Migrate restore run check severity badges to central mapping in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/resources/views/filament/forms/components/restore-run-checks.blade.php`
|
||||||
|
- [X] T030 [US1] Sweep + migrate remaining status-like badge semantics in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/`, `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Livewire/`, and `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/resources/views/filament/` (status/health + severity/risk only; tag/category chips explicitly out of scope for v1)
|
||||||
|
|
||||||
|
**Checkpoint**: US1 is shippable as an MVP (status-like badges consistent across key surfaces).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: User Story 2 — Readable status badges in dark mode (Priority: P2)
|
||||||
|
|
||||||
|
**Goal**: Status-like badges remain readable in dark mode, without custom Tailwind chip overrides.
|
||||||
|
|
||||||
|
**Independent Test**: Open restore preview/results and other badge-heavy views in dark mode and confirm badges remain readable and consistent.
|
||||||
|
|
||||||
|
### Tests (US2)
|
||||||
|
|
||||||
|
- [X] T031 [P] [US2] Add restore preview/results decision/status mapping tests in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Unit/Badges/RestoreUiBadgesTest.php`
|
||||||
|
|
||||||
|
### Implementation (US2)
|
||||||
|
|
||||||
|
- [X] T032 [US2] Introduce restore preview decision badge domain in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/Domains/RestorePreviewDecisionBadge.php`
|
||||||
|
- [X] T033 [US2] Introduce restore results status badge domain in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/Domains/RestoreResultStatusBadge.php`
|
||||||
|
- [X] T034 [US2] Replace custom Tailwind decision chips with Filament badges in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/resources/views/filament/infolists/entries/restore-preview.blade.php` (status-like chips only; keep policy type/platform tags as-is)
|
||||||
|
- [X] T035 [US2] Replace custom Tailwind result/status chips with Filament badges in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/resources/views/filament/infolists/entries/restore-results.blade.php` (status-like chips only)
|
||||||
|
|
||||||
|
**Checkpoint**: Dark mode readability is preserved on badge-heavy restore views.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5: User Story 3 — Consistency stays enforced over time (Priority: P3)
|
||||||
|
|
||||||
|
**Goal**: Ad-hoc status-like badge semantics cannot be reintroduced without failing automated checks.
|
||||||
|
|
||||||
|
**Independent Test**: Introduce an ad-hoc status/severity mapping in a Filament surface and confirm the guard test fails.
|
||||||
|
|
||||||
|
### Tests + Guard (US3)
|
||||||
|
|
||||||
|
- [X] T036 [US3] Add “no ad-hoc status-like badge semantics” guard test in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Feature/Guards/NoAdHocStatusBadgesTest.php` (pattern-based; allowlist tag/category chips)
|
||||||
|
|
||||||
|
**Checkpoint**: Guardrails prevent drift from reappearing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 6: Polish & Cross-Cutting Concerns
|
||||||
|
|
||||||
|
- [X] T037 [P] Run formatting on changed files via `vendor/bin/sail php /Users/ahmeddarrazi/Documents/projects/TenantAtlas/vendor/bin/pint --dirty`
|
||||||
|
- [X] T038 Run targeted tests via `vendor/bin/sail artisan test /Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Unit/Badges/ /Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Feature/Guards/NoAdHocStatusBadgesTest.php`
|
||||||
|
- [X] T039 Run quickstart verification steps from `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/059-unified-badges/quickstart.md`
|
||||||
|
- [X] T040 [P] Add BackupSet status mapping tests in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Unit/Badges/BackupSetBadgesTest.php` (completed must be success)
|
||||||
|
- [X] T041 Migrate BackupSet status badge to central mapping in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Resources/BackupSetResource.php` (completed must be green)
|
||||||
|
- [X] T042 Sweep for any remaining `completed` status-like badges rendered as warning and migrate to BadgeCatalog
|
||||||
|
- [X] T043 Fix Blade compilation for restore views (replace inline `@php(...)`) in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/resources/views/filament/infolists/entries/restore-results.blade.php`, `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/resources/views/filament/infolists/entries/restore-preview.blade.php`, and `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/resources/views/filament/forms/components/restore-run-checks.blade.php`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies & Execution Order
|
||||||
|
|
||||||
|
### Dependency Graph (high-level)
|
||||||
|
|
||||||
|
```text
|
||||||
|
Phase 1 (Setup)
|
||||||
|
↓
|
||||||
|
Phase 2 (Foundational badge system)
|
||||||
|
↓
|
||||||
|
US1 (status/health + severity/risk migration) ──┬─→ US2 (dark mode restore views)
|
||||||
|
└─→ US3 (guard test)
|
||||||
|
↓
|
||||||
|
Phase 6 (Polish)
|
||||||
|
```
|
||||||
|
|
||||||
|
### User Story Dependencies
|
||||||
|
|
||||||
|
- US1 (P1) depends on Foundational (Phase 2) and can ship as the MVP.
|
||||||
|
- US2 (P2) depends on Foundational (Phase 2); easiest after US1 establishes the core domains.
|
||||||
|
- US3 (P3) depends on Foundational (Phase 2) and should run after the first migrations to tune allowlists and reduce false positives.
|
||||||
|
|
||||||
|
Suggested MVP-first order: Phase 1 → Phase 2 → US1 → Phase 6 (minimum) → US2 → US3 → Phase 6 (final pass).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Parallel Execution Examples (per user story)
|
||||||
|
|
||||||
|
### US1
|
||||||
|
- T008 [P] [US1] `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Unit/Badges/OperationRunBadgesTest.php`
|
||||||
|
- T009 [P] [US1] `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Unit/Badges/FindingBadgesTest.php`
|
||||||
|
- T010 [P] [US1] `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Unit/Badges/RestoreRunBadgesTest.php`
|
||||||
|
- T011 [P] [US1] `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Unit/Badges/RunStatusBadgesTest.php`
|
||||||
|
- T020–T029 [P] [US1] (migration tasks; different files)
|
||||||
|
|
||||||
|
### US2
|
||||||
|
- T031 [P] [US2] `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Unit/Badges/RestoreUiBadgesTest.php`
|
||||||
|
|
||||||
|
### US3
|
||||||
|
- T036 [US3] `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Feature/Guards/NoAdHocStatusBadgesTest.php`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Strategy
|
||||||
|
|
||||||
|
### MVP First (US1 only)
|
||||||
|
|
||||||
|
1. Complete Phase 1 + Phase 2
|
||||||
|
2. Implement US1 migrations for Operations + Drift findings + Restore runs first (T020–T029)
|
||||||
|
3. Run: `vendor/bin/sail artisan test /Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Unit/Badges/OperationRunBadgesTest.php`
|
||||||
|
4. Run: `vendor/bin/sail artisan test /Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Unit/Badges/FindingBadgesTest.php`
|
||||||
|
|
||||||
|
### Incremental Delivery
|
||||||
|
|
||||||
|
- Finish US1 sweep (T030), then address restore Blade dark-mode hotspots in US2, then lock in regression prevention in US3.
|
||||||
115
tests/Feature/Guards/NoAdHocStatusBadgesTest.php
Normal file
115
tests/Feature/Guards/NoAdHocStatusBadgesTest.php
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
it('does not contain ad-hoc status-like badge semantics', function () {
|
||||||
|
$root = base_path();
|
||||||
|
$self = realpath(__FILE__);
|
||||||
|
|
||||||
|
$directories = [
|
||||||
|
$root.'/app/Filament',
|
||||||
|
$root.'/app/Livewire',
|
||||||
|
];
|
||||||
|
|
||||||
|
$excludedPaths = [
|
||||||
|
$root.'/vendor',
|
||||||
|
$root.'/storage',
|
||||||
|
$root.'/specs',
|
||||||
|
$root.'/spechistory',
|
||||||
|
$root.'/references',
|
||||||
|
$root.'/public/build',
|
||||||
|
];
|
||||||
|
|
||||||
|
$statusLikeTokenPattern = '/[\'"](?:queued|running|completed|pending|succeeded|partial|failed|cancelled|canceled|applied|dry_run|manual_required|mapped_existing|created|created_copy|skipped|blocking|acknowledged|new|low|medium|high)[\'"]/';
|
||||||
|
$inlineColorStartPattern = '/->color\\s*\\(\\s*(?:fn|function)\\b/';
|
||||||
|
$inlineLabelStartPattern = '/->formatStateUsing\\s*\\(\\s*(?:fn|function)\\b/';
|
||||||
|
|
||||||
|
$forbiddenPlainPatterns = [
|
||||||
|
'/\\bBadgeColumn::make\\b/',
|
||||||
|
'/->colors\\s*\\(/',
|
||||||
|
];
|
||||||
|
|
||||||
|
$lookaheadLines = 25;
|
||||||
|
|
||||||
|
/** @var Collection<int, string> $files */
|
||||||
|
$files = collect($directories)
|
||||||
|
->filter(fn (string $dir): bool => is_dir($dir))
|
||||||
|
->flatMap(function (string $dir): array {
|
||||||
|
$iterator = new RecursiveIteratorIterator(
|
||||||
|
new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS)
|
||||||
|
);
|
||||||
|
|
||||||
|
$paths = [];
|
||||||
|
|
||||||
|
foreach ($iterator as $file) {
|
||||||
|
if (! $file->isFile()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = $file->getPathname();
|
||||||
|
|
||||||
|
if (! str_ends_with($path, '.php')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$paths[] = $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $paths;
|
||||||
|
})
|
||||||
|
->filter(function (string $path) use ($excludedPaths, $self): bool {
|
||||||
|
if ($self && realpath($path) === $self) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($excludedPaths as $excluded) {
|
||||||
|
if (str_starts_with($path, $excluded)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
->values();
|
||||||
|
|
||||||
|
$hits = [];
|
||||||
|
|
||||||
|
foreach ($files as $path) {
|
||||||
|
$contents = file_get_contents($path);
|
||||||
|
|
||||||
|
if (! is_string($contents) || $contents === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = preg_split('/\R/', $contents) ?: [];
|
||||||
|
|
||||||
|
foreach ($lines as $index => $line) {
|
||||||
|
foreach ($forbiddenPlainPatterns as $pattern) {
|
||||||
|
if (preg_match($pattern, $line)) {
|
||||||
|
$relative = str_replace($root.'/', '', $path);
|
||||||
|
$hits[] = $relative.':'.($index + 1).' -> '.trim($line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match($inlineColorStartPattern, $line)) {
|
||||||
|
$window = implode("\n", array_slice($lines, $index, $lookaheadLines));
|
||||||
|
|
||||||
|
if (preg_match($statusLikeTokenPattern, $window)) {
|
||||||
|
$relative = str_replace($root.'/', '', $path);
|
||||||
|
$hits[] = $relative.':'.($index + 1).' -> '.trim($line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match($inlineLabelStartPattern, $line)) {
|
||||||
|
$window = implode("\n", array_slice($lines, $index, $lookaheadLines));
|
||||||
|
|
||||||
|
if (preg_match($statusLikeTokenPattern, $window)) {
|
||||||
|
$relative = str_replace($root.'/', '', $path);
|
||||||
|
$hits[] = $relative.':'.($index + 1).' -> '.trim($line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect($hits)->toBeEmpty("Ad-hoc status-like badge semantics found:\n".implode("\n", $hits));
|
||||||
|
});
|
||||||
24
tests/Unit/Badges/BackupSetBadgesTest.php
Normal file
24
tests/Unit/Badges/BackupSetBadgesTest.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeDomain;
|
||||||
|
|
||||||
|
it('maps backup set status values to canonical badge semantics', function (): void {
|
||||||
|
$running = BadgeCatalog::spec(BadgeDomain::BackupSetStatus, 'running');
|
||||||
|
expect($running->label)->toBe('Running');
|
||||||
|
expect($running->color)->toBe('info');
|
||||||
|
|
||||||
|
$completed = BadgeCatalog::spec(BadgeDomain::BackupSetStatus, 'completed');
|
||||||
|
expect($completed->label)->toBe('Completed');
|
||||||
|
expect($completed->color)->toBe('success');
|
||||||
|
|
||||||
|
$partial = BadgeCatalog::spec(BadgeDomain::BackupSetStatus, 'partial');
|
||||||
|
expect($partial->label)->toBe('Partial');
|
||||||
|
expect($partial->color)->toBe('warning');
|
||||||
|
|
||||||
|
$failed = BadgeCatalog::spec(BadgeDomain::BackupSetStatus, 'failed');
|
||||||
|
expect($failed->label)->toBe('Failed');
|
||||||
|
expect($failed->color)->toBe('danger');
|
||||||
|
});
|
||||||
26
tests/Unit/Badges/BadgeCatalogTest.php
Normal file
26
tests/Unit/Badges/BadgeCatalogTest.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeDomain;
|
||||||
|
use App\Support\Badges\BadgeSpec;
|
||||||
|
|
||||||
|
it('returns a safe unknown badge spec for unknown values', function (): void {
|
||||||
|
$spec = BadgeCatalog::spec(BadgeDomain::OperationRunStatus, 'not-a-real-status');
|
||||||
|
|
||||||
|
expect($spec)->toBeInstanceOf(BadgeSpec::class);
|
||||||
|
expect($spec->label)->toBe('Unknown');
|
||||||
|
expect($spec->color)->toBe('gray');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defines the allowed Filament badge colors', function (): void {
|
||||||
|
expect(BadgeSpec::allowedColors())->toBe([
|
||||||
|
'gray',
|
||||||
|
'info',
|
||||||
|
'success',
|
||||||
|
'warning',
|
||||||
|
'danger',
|
||||||
|
'primary',
|
||||||
|
]);
|
||||||
|
});
|
||||||
30
tests/Unit/Badges/FindingBadgesTest.php
Normal file
30
tests/Unit/Badges/FindingBadgesTest.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeDomain;
|
||||||
|
|
||||||
|
it('maps finding severity values to canonical badge semantics', function (): void {
|
||||||
|
$low = BadgeCatalog::spec(BadgeDomain::FindingSeverity, 'low');
|
||||||
|
expect($low->label)->toBe('Low');
|
||||||
|
expect($low->color)->toBe('gray');
|
||||||
|
|
||||||
|
$medium = BadgeCatalog::spec(BadgeDomain::FindingSeverity, 'medium');
|
||||||
|
expect($medium->label)->toBe('Medium');
|
||||||
|
expect($medium->color)->toBe('warning');
|
||||||
|
|
||||||
|
$high = BadgeCatalog::spec(BadgeDomain::FindingSeverity, 'high');
|
||||||
|
expect($high->label)->toBe('High');
|
||||||
|
expect($high->color)->toBe('danger');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps finding status values to canonical badge semantics', function (): void {
|
||||||
|
$new = BadgeCatalog::spec(BadgeDomain::FindingStatus, 'new');
|
||||||
|
expect($new->label)->toBe('New');
|
||||||
|
expect($new->color)->toBe('warning');
|
||||||
|
|
||||||
|
$acknowledged = BadgeCatalog::spec(BadgeDomain::FindingStatus, 'acknowledged');
|
||||||
|
expect($acknowledged->label)->toBe('Acknowledged');
|
||||||
|
expect($acknowledged->color)->toBe('gray');
|
||||||
|
});
|
||||||
48
tests/Unit/Badges/OperationRunBadgesTest.php
Normal file
48
tests/Unit/Badges/OperationRunBadgesTest.php
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeDomain;
|
||||||
|
|
||||||
|
it('maps operation run status values to canonical badge semantics', function (): void {
|
||||||
|
$queued = BadgeCatalog::spec(BadgeDomain::OperationRunStatus, 'queued');
|
||||||
|
expect($queued->label)->toBe('Queued');
|
||||||
|
expect($queued->color)->toBe('warning');
|
||||||
|
|
||||||
|
$running = BadgeCatalog::spec(BadgeDomain::OperationRunStatus, 'running');
|
||||||
|
expect($running->label)->toBe('Running');
|
||||||
|
expect($running->color)->toBe('info');
|
||||||
|
|
||||||
|
$completed = BadgeCatalog::spec(BadgeDomain::OperationRunStatus, 'completed');
|
||||||
|
expect($completed->label)->toBe('Completed');
|
||||||
|
expect($completed->color)->toBe('gray');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps operation run outcome values to canonical badge semantics', function (): void {
|
||||||
|
$pending = BadgeCatalog::spec(BadgeDomain::OperationRunOutcome, 'pending');
|
||||||
|
expect($pending->label)->toBe('Pending');
|
||||||
|
expect($pending->color)->toBe('gray');
|
||||||
|
|
||||||
|
$succeeded = BadgeCatalog::spec(BadgeDomain::OperationRunOutcome, 'succeeded');
|
||||||
|
expect($succeeded->label)->toBe('Succeeded');
|
||||||
|
expect($succeeded->color)->toBe('success');
|
||||||
|
|
||||||
|
$partial = BadgeCatalog::spec(BadgeDomain::OperationRunOutcome, 'partially_succeeded');
|
||||||
|
expect($partial->label)->toBe('Partially succeeded');
|
||||||
|
expect($partial->color)->toBe('warning');
|
||||||
|
|
||||||
|
$failed = BadgeCatalog::spec(BadgeDomain::OperationRunOutcome, 'failed');
|
||||||
|
expect($failed->label)->toBe('Failed');
|
||||||
|
expect($failed->color)->toBe('danger');
|
||||||
|
|
||||||
|
$cancelled = BadgeCatalog::spec(BadgeDomain::OperationRunOutcome, 'cancelled');
|
||||||
|
expect($cancelled->label)->toBe('Cancelled');
|
||||||
|
expect($cancelled->color)->toBe('gray');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('never represents a success outcome with warning/attention meaning', function (): void {
|
||||||
|
$succeeded = BadgeCatalog::spec(BadgeDomain::OperationRunOutcome, 'succeeded');
|
||||||
|
|
||||||
|
expect($succeeded->color)->not->toBe('warning');
|
||||||
|
});
|
||||||
46
tests/Unit/Badges/PolicyBadgesTest.php
Normal file
46
tests/Unit/Badges/PolicyBadgesTest.php
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeDomain;
|
||||||
|
|
||||||
|
it('maps policy snapshot mode values to canonical badge semantics', function (): void {
|
||||||
|
$full = BadgeCatalog::spec(BadgeDomain::PolicySnapshotMode, 'full');
|
||||||
|
expect($full->label)->toBe('Full');
|
||||||
|
expect($full->color)->toBe('success');
|
||||||
|
|
||||||
|
$metadataOnly = BadgeCatalog::spec(BadgeDomain::PolicySnapshotMode, 'metadata_only');
|
||||||
|
expect($metadataOnly->label)->toBe('Metadata only');
|
||||||
|
expect($metadataOnly->color)->toBe('warning');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps policy restore mode values to canonical badge semantics', function (): void {
|
||||||
|
$enabled = BadgeCatalog::spec(BadgeDomain::PolicyRestoreMode, 'enabled');
|
||||||
|
expect($enabled->label)->toBe('Enabled');
|
||||||
|
expect($enabled->color)->toBe('success');
|
||||||
|
|
||||||
|
$previewOnly = BadgeCatalog::spec(BadgeDomain::PolicyRestoreMode, 'preview-only');
|
||||||
|
expect($previewOnly->label)->toBe('Preview only');
|
||||||
|
expect($previewOnly->color)->toBe('warning');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps policy risk values to canonical badge semantics', function (): void {
|
||||||
|
$low = BadgeCatalog::spec(BadgeDomain::PolicyRisk, 'low');
|
||||||
|
expect($low->label)->toBe('Low');
|
||||||
|
expect($low->color)->toBe('gray');
|
||||||
|
|
||||||
|
$mediumHigh = BadgeCatalog::spec(BadgeDomain::PolicyRisk, 'medium-high');
|
||||||
|
expect($mediumHigh->label)->toBe('Medium-high');
|
||||||
|
expect($mediumHigh->color)->toBe('danger');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps ignored-at presence to canonical badge semantics', function (): void {
|
||||||
|
$notIgnored = BadgeCatalog::spec(BadgeDomain::IgnoredAt, null);
|
||||||
|
expect($notIgnored->label)->toBe('No');
|
||||||
|
expect($notIgnored->color)->toBe('gray');
|
||||||
|
|
||||||
|
$ignored = BadgeCatalog::spec(BadgeDomain::IgnoredAt, '2026-01-01T00:00:00Z');
|
||||||
|
expect($ignored->label)->toBe('Yes');
|
||||||
|
expect($ignored->color)->toBe('warning');
|
||||||
|
});
|
||||||
56
tests/Unit/Badges/RestoreRunBadgesTest.php
Normal file
56
tests/Unit/Badges/RestoreRunBadgesTest.php
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeDomain;
|
||||||
|
|
||||||
|
it('maps restore run status values to canonical badge semantics', function (): void {
|
||||||
|
$draft = BadgeCatalog::spec(BadgeDomain::RestoreRunStatus, 'draft');
|
||||||
|
expect($draft->label)->toBe('Draft');
|
||||||
|
expect($draft->color)->toBe('gray');
|
||||||
|
|
||||||
|
$previewed = BadgeCatalog::spec(BadgeDomain::RestoreRunStatus, 'previewed');
|
||||||
|
expect($previewed->label)->toBe('Previewed');
|
||||||
|
expect($previewed->color)->toBe('gray');
|
||||||
|
|
||||||
|
$queued = BadgeCatalog::spec(BadgeDomain::RestoreRunStatus, 'queued');
|
||||||
|
expect($queued->label)->toBe('Queued');
|
||||||
|
expect($queued->color)->toBe('warning');
|
||||||
|
|
||||||
|
$running = BadgeCatalog::spec(BadgeDomain::RestoreRunStatus, 'running');
|
||||||
|
expect($running->label)->toBe('Running');
|
||||||
|
expect($running->color)->toBe('info');
|
||||||
|
|
||||||
|
$completed = BadgeCatalog::spec(BadgeDomain::RestoreRunStatus, 'completed');
|
||||||
|
expect($completed->label)->toBe('Completed');
|
||||||
|
expect($completed->color)->toBe('success');
|
||||||
|
|
||||||
|
$partial = BadgeCatalog::spec(BadgeDomain::RestoreRunStatus, 'partial');
|
||||||
|
expect($partial->label)->toBe('Partial');
|
||||||
|
expect($partial->color)->toBe('warning');
|
||||||
|
|
||||||
|
$failed = BadgeCatalog::spec(BadgeDomain::RestoreRunStatus, 'failed');
|
||||||
|
expect($failed->label)->toBe('Failed');
|
||||||
|
expect($failed->color)->toBe('danger');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('never represents a completed outcome with warning/attention meaning', function (): void {
|
||||||
|
$completed = BadgeCatalog::spec(BadgeDomain::RestoreRunStatus, 'completed');
|
||||||
|
|
||||||
|
expect($completed->color)->not->toBe('warning');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps restore safety check severity values to canonical badge semantics', function (): void {
|
||||||
|
$blocking = BadgeCatalog::spec(BadgeDomain::RestoreCheckSeverity, 'blocking');
|
||||||
|
expect($blocking->label)->toBe('Blocking');
|
||||||
|
expect($blocking->color)->toBe('danger');
|
||||||
|
|
||||||
|
$warning = BadgeCatalog::spec(BadgeDomain::RestoreCheckSeverity, 'warning');
|
||||||
|
expect($warning->label)->toBe('Warning');
|
||||||
|
expect($warning->color)->toBe('warning');
|
||||||
|
|
||||||
|
$safe = BadgeCatalog::spec(BadgeDomain::RestoreCheckSeverity, 'safe');
|
||||||
|
expect($safe->label)->toBe('Safe');
|
||||||
|
expect($safe->color)->toBe('success');
|
||||||
|
});
|
||||||
38
tests/Unit/Badges/RestoreUiBadgesTest.php
Normal file
38
tests/Unit/Badges/RestoreUiBadgesTest.php
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeDomain;
|
||||||
|
|
||||||
|
it('maps restore preview decisions to canonical badge semantics', function (): void {
|
||||||
|
$created = BadgeCatalog::spec(BadgeDomain::RestorePreviewDecision, 'created');
|
||||||
|
expect($created->label)->toBe('Created');
|
||||||
|
expect($created->color)->toBe('success');
|
||||||
|
|
||||||
|
$mappedExisting = BadgeCatalog::spec(BadgeDomain::RestorePreviewDecision, 'mapped_existing');
|
||||||
|
expect($mappedExisting->label)->toBe('Mapped existing');
|
||||||
|
expect($mappedExisting->color)->toBe('info');
|
||||||
|
|
||||||
|
$failed = BadgeCatalog::spec(BadgeDomain::RestorePreviewDecision, 'failed');
|
||||||
|
expect($failed->label)->toBe('Failed');
|
||||||
|
expect($failed->color)->toBe('danger');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps restore results statuses to canonical badge semantics', function (): void {
|
||||||
|
$applied = BadgeCatalog::spec(BadgeDomain::RestoreResultStatus, 'applied');
|
||||||
|
expect($applied->label)->toBe('Applied');
|
||||||
|
expect($applied->color)->toBe('success');
|
||||||
|
|
||||||
|
$dryRun = BadgeCatalog::spec(BadgeDomain::RestoreResultStatus, 'dry_run');
|
||||||
|
expect($dryRun->label)->toBe('Dry run');
|
||||||
|
expect($dryRun->color)->toBe('info');
|
||||||
|
|
||||||
|
$manualRequired = BadgeCatalog::spec(BadgeDomain::RestoreResultStatus, 'manual_required');
|
||||||
|
expect($manualRequired->label)->toBe('Manual required');
|
||||||
|
expect($manualRequired->color)->toBe('warning');
|
||||||
|
|
||||||
|
$failed = BadgeCatalog::spec(BadgeDomain::RestoreResultStatus, 'failed');
|
||||||
|
expect($failed->label)->toBe('Failed');
|
||||||
|
expect($failed->color)->toBe('danger');
|
||||||
|
});
|
||||||
58
tests/Unit/Badges/RunStatusBadgesTest.php
Normal file
58
tests/Unit/Badges/RunStatusBadgesTest.php
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeDomain;
|
||||||
|
|
||||||
|
it('maps inventory sync run status values to canonical badge semantics', function (): void {
|
||||||
|
$pending = BadgeCatalog::spec(BadgeDomain::InventorySyncRunStatus, 'pending');
|
||||||
|
expect($pending->label)->toBe('Pending');
|
||||||
|
expect($pending->color)->toBe('gray');
|
||||||
|
|
||||||
|
$running = BadgeCatalog::spec(BadgeDomain::InventorySyncRunStatus, 'running');
|
||||||
|
expect($running->label)->toBe('Running');
|
||||||
|
expect($running->color)->toBe('info');
|
||||||
|
|
||||||
|
$success = BadgeCatalog::spec(BadgeDomain::InventorySyncRunStatus, 'success');
|
||||||
|
expect($success->label)->toBe('Success');
|
||||||
|
expect($success->color)->toBe('success');
|
||||||
|
|
||||||
|
$partial = BadgeCatalog::spec(BadgeDomain::InventorySyncRunStatus, 'partial');
|
||||||
|
expect($partial->label)->toBe('Partial');
|
||||||
|
expect($partial->color)->toBe('warning');
|
||||||
|
|
||||||
|
$failed = BadgeCatalog::spec(BadgeDomain::InventorySyncRunStatus, 'failed');
|
||||||
|
expect($failed->label)->toBe('Failed');
|
||||||
|
expect($failed->color)->toBe('danger');
|
||||||
|
|
||||||
|
$skipped = BadgeCatalog::spec(BadgeDomain::InventorySyncRunStatus, 'skipped');
|
||||||
|
expect($skipped->label)->toBe('Skipped');
|
||||||
|
expect($skipped->color)->toBe('gray');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps backup schedule run status values to canonical badge semantics', function (): void {
|
||||||
|
$running = BadgeCatalog::spec(BadgeDomain::BackupScheduleRunStatus, 'running');
|
||||||
|
expect($running->label)->toBe('Running');
|
||||||
|
expect($running->color)->toBe('info');
|
||||||
|
|
||||||
|
$success = BadgeCatalog::spec(BadgeDomain::BackupScheduleRunStatus, 'success');
|
||||||
|
expect($success->label)->toBe('Success');
|
||||||
|
expect($success->color)->toBe('success');
|
||||||
|
|
||||||
|
$partial = BadgeCatalog::spec(BadgeDomain::BackupScheduleRunStatus, 'partial');
|
||||||
|
expect($partial->label)->toBe('Partial');
|
||||||
|
expect($partial->color)->toBe('warning');
|
||||||
|
|
||||||
|
$failed = BadgeCatalog::spec(BadgeDomain::BackupScheduleRunStatus, 'failed');
|
||||||
|
expect($failed->label)->toBe('Failed');
|
||||||
|
expect($failed->color)->toBe('danger');
|
||||||
|
|
||||||
|
$canceled = BadgeCatalog::spec(BadgeDomain::BackupScheduleRunStatus, 'canceled');
|
||||||
|
expect($canceled->label)->toBe('Canceled');
|
||||||
|
expect($canceled->color)->toBe('gray');
|
||||||
|
|
||||||
|
$skipped = BadgeCatalog::spec(BadgeDomain::BackupScheduleRunStatus, 'skipped');
|
||||||
|
expect($skipped->label)->toBe('Skipped');
|
||||||
|
expect($skipped->color)->toBe('gray');
|
||||||
|
});
|
||||||
66
tests/Unit/Badges/TenantBadgesTest.php
Normal file
66
tests/Unit/Badges/TenantBadgesTest.php
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeDomain;
|
||||||
|
|
||||||
|
it('maps tenant status values to canonical badge semantics', function (): void {
|
||||||
|
$active = BadgeCatalog::spec(BadgeDomain::TenantStatus, 'active');
|
||||||
|
expect($active->label)->toBe('Active');
|
||||||
|
expect($active->color)->toBe('success');
|
||||||
|
|
||||||
|
$archived = BadgeCatalog::spec(BadgeDomain::TenantStatus, 'archived');
|
||||||
|
expect($archived->label)->toBe('Archived');
|
||||||
|
expect($archived->color)->toBe('gray');
|
||||||
|
|
||||||
|
$suspended = BadgeCatalog::spec(BadgeDomain::TenantStatus, 'suspended');
|
||||||
|
expect($suspended->label)->toBe('Suspended');
|
||||||
|
expect($suspended->color)->toBe('warning');
|
||||||
|
|
||||||
|
$error = BadgeCatalog::spec(BadgeDomain::TenantStatus, 'error');
|
||||||
|
expect($error->label)->toBe('Error');
|
||||||
|
expect($error->color)->toBe('danger');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps tenant app status values to canonical badge semantics', function (): void {
|
||||||
|
$ok = BadgeCatalog::spec(BadgeDomain::TenantAppStatus, 'ok');
|
||||||
|
expect($ok->label)->toBe('OK');
|
||||||
|
expect($ok->color)->toBe('success');
|
||||||
|
|
||||||
|
$consentRequired = BadgeCatalog::spec(BadgeDomain::TenantAppStatus, 'consent_required');
|
||||||
|
expect($consentRequired->label)->toBe('Consent required');
|
||||||
|
expect($consentRequired->color)->toBe('warning');
|
||||||
|
|
||||||
|
$error = BadgeCatalog::spec(BadgeDomain::TenantAppStatus, 'error');
|
||||||
|
expect($error->label)->toBe('Error');
|
||||||
|
expect($error->color)->toBe('danger');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps tenant RBAC status values to canonical badge semantics', function (): void {
|
||||||
|
$configured = BadgeCatalog::spec(BadgeDomain::TenantRbacStatus, 'configured');
|
||||||
|
expect($configured->label)->toBe('Configured');
|
||||||
|
expect($configured->color)->toBe('success');
|
||||||
|
|
||||||
|
$manual = BadgeCatalog::spec(BadgeDomain::TenantRbacStatus, 'manual_assignment_required');
|
||||||
|
expect($manual->label)->toBe('Manual assignment required');
|
||||||
|
expect($manual->color)->toBe('warning');
|
||||||
|
|
||||||
|
$notConfigured = BadgeCatalog::spec(BadgeDomain::TenantRbacStatus, 'not_configured');
|
||||||
|
expect($notConfigured->label)->toBe('Not configured');
|
||||||
|
expect($notConfigured->color)->toBe('gray');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps tenant permission status values to canonical badge semantics', function (): void {
|
||||||
|
$granted = BadgeCatalog::spec(BadgeDomain::TenantPermissionStatus, 'granted');
|
||||||
|
expect($granted->label)->toBe('Granted');
|
||||||
|
expect($granted->color)->toBe('success');
|
||||||
|
|
||||||
|
$missing = BadgeCatalog::spec(BadgeDomain::TenantPermissionStatus, 'missing');
|
||||||
|
expect($missing->label)->toBe('Missing');
|
||||||
|
expect($missing->color)->toBe('warning');
|
||||||
|
|
||||||
|
$error = BadgeCatalog::spec(BadgeDomain::TenantPermissionStatus, 'error');
|
||||||
|
expect($error->label)->toBe('Error');
|
||||||
|
expect($error->color)->toBe('danger');
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user