diff --git a/app/Filament/Resources/InventoryItemResource.php b/app/Filament/Resources/InventoryItemResource.php index 265254f..564c73f 100644 --- a/app/Filament/Resources/InventoryItemResource.php +++ b/app/Filament/Resources/InventoryItemResource.php @@ -100,6 +100,10 @@ public static function infolist(Schema $schema): Schema } return $resolver->attachRenderedTargets($edges->take(100), $tenant); // both directions combined + + return $edges->take(100); // both directions combined + + return $resolver->attachRenderedTargets($edges->take(100), $tenant); // both directions combined }) ->columnSpanFull(), ]) diff --git a/app/Services/Inventory/DependencyExtractionService.php b/app/Services/Inventory/DependencyExtractionService.php index 46e7119..90aed57 100644 --- a/app/Services/Inventory/DependencyExtractionService.php +++ b/app/Services/Inventory/DependencyExtractionService.php @@ -25,6 +25,7 @@ public function extractForPolicyData(InventoryItem $item, array $policyData): ar $edges = $edges->merge($this->extractAssignments($item, $policyData, $warnings)); $edges = $edges->merge($this->extractScopedBy($item, $policyData)); + // Prevent PostgreSQL cardinality violation on upsert by deduplicating payload rows. $edges = $edges ->unique(fn (array $e) => implode('|', [ (string) ($e['tenant_id'] ?? ''), @@ -40,10 +41,11 @@ public function extractForPolicyData(InventoryItem $item, array $policyData): ar $priorities = [ RelationshipType::AssignedToInclude->value => 1, RelationshipType::AssignedToExclude->value => 2, - RelationshipType::UsesAssignmentFilter->value => 3, - RelationshipType::ScopedBy->value => 2, - RelationshipType::Targets->value => 3, - RelationshipType::DependsOn->value => 4, + RelationshipType::AssignedTo->value => 3, // legacy + RelationshipType::UsesAssignmentFilter->value => 4, + RelationshipType::ScopedBy->value => 5, + RelationshipType::Targets->value => 6, + RelationshipType::DependsOn->value => 7, ]; /** @var Collection $sorted */ @@ -135,7 +137,6 @@ private function extractAssignments(InventoryItem $item, array $policyData, arra if (is_string($odataType) && str_contains($odataType, 'exclusion')) { $relationshipType = RelationshipType::AssignedToExclude->value; } - $edges[] = [ 'tenant_id' => (int) $item->tenant_id, 'source_type' => 'inventory_item', diff --git a/resources/views/filament/components/dependency-edges.blade.php b/resources/views/filament/components/dependency-edges.blade.php index 47d4bdd..146913c 100644 --- a/resources/views/filament/components/dependency-edges.blade.php +++ b/resources/views/filament/components/dependency-edges.blade.php @@ -52,12 +52,25 @@ $missingTitle .= '. Ref: '.\Illuminate\Support\Str::limit($encodedRef, 200); } } + + $fallbackDisplay = null; + if (is_string($lastKnownName) && $lastKnownName !== '') { + $fallbackDisplay = $lastKnownName; + } elseif (is_string($targetId) && $targetId !== '') { + $fallbackDisplay = 'ID: '.substr($targetId, 0, 6).'…'; + } else { + $fallbackDisplay = 'External reference'; + } @endphp
  • - @if (is_string($linkUrl) && $linkUrl !== '') - {{ is_string($badgeText) && $badgeText !== '' ? $badgeText : 'External reference' }} + @if (is_string($badgeText) && $badgeText !== '') + @if (is_string($linkUrl) && $linkUrl !== '') + {{ $badgeText }} + @else + {{ $badgeText }} + @endif @else - {{ is_string($badgeText) && $badgeText !== '' ? $badgeText : 'External reference' }} + {{ $fallbackDisplay }} @endif @if ($isMissing) Missing diff --git a/specs/042-inventory-dependencies-graph/data-model.md b/specs/042-inventory-dependencies-graph/data-model.md index 785163d..53148b5 100644 --- a/specs/042-inventory-dependencies-graph/data-model.md +++ b/specs/042-inventory-dependencies-graph/data-model.md @@ -50,11 +50,9 @@ #### InventoryLink.metadata - `raw_ref` (mixed/array; only when safe) Required when `target_type='foundation_object'`: -- `foundation_type` (string enum-like): `aad_group` | `scope_tag` | `device_category` - +- `foundation_type` (string enum-like): `aad_group` | `scope_tag` | `device_category` | `assignment_filter` Additional metadata (when applicable): - `filter_mode` (string enum-like): `include` | `exclude` (for `foundation_type='assignment_filter'`) - ## Enums ### RelationshipType diff --git a/specs/042-inventory-dependencies-graph/spec.md b/specs/042-inventory-dependencies-graph/spec.md index d9e879f..1dde711 100644 --- a/specs/042-inventory-dependencies-graph/spec.md +++ b/specs/042-inventory-dependencies-graph/spec.md @@ -24,7 +24,6 @@ ### Session 2026-01-10 (042.2) - Q: Should the Dependencies UI do Entra/Graph lookups to resolve names (e.g., Groups)? → A: No. UI resolution is DB-only. - Q: How should the UI avoid "Unknown" targets long-term? → A: Render a stable, typed DTO per edge target via a resolver layer (batch queries, no N+1). - **Definitions**: - **Blast radius**: All resources directly affected by a change to a given item (outbound edges only; no transitive traversal in MVP). - **Prerequisite**: A hard dependency required for an item to function; missing prerequisites are explicitly surfaced. @@ -59,7 +58,6 @@ ### Scenario 6 (042.2): Resolve target names when available - Then each edge target is labelled deterministically (no "Unknown") - And names are resolved only when the target exists in the local DB (no UI Graph lookups) - And external references (AAD groups) are rendered as external refs without links - ### Scenario 5: Only missing prerequisites - Given an item where all referenced targets are unresolved (no matching inventory or foundation objects) - When the user opens the dependencies view and selects "Outbound" or "All" diff --git a/specs/042-inventory-dependencies-graph/tasks.md b/specs/042-inventory-dependencies-graph/tasks.md index 8bc4356..1daffc2 100644 --- a/specs/042-inventory-dependencies-graph/tasks.md +++ b/specs/042-inventory-dependencies-graph/tasks.md @@ -21,17 +21,6 @@ ## Phase 2: Foundational (Blocking Prerequisites) **Purpose**: Storage + extraction + query services that all UI stories rely on. **Checkpoint**: After Phase 2, edges can be extracted and queried tenant-safely with limits. - -- [ ] T003 [P] Ensure relationship types and labels are centralized in `app/Support/Enums/RelationshipType.php` -- [ ] T004 Ensure `inventory_links` schema (unique key + indexes) matches spec in `database/migrations/2026_01_07_150000_create_inventory_links_table.php` -- [ ] T005 [P] Ensure `InventoryLink` casts and tenant-safe query patterns in `app/Models/InventoryLink.php` -- [ ] T006 [P] Ensure factory coverage for dependency edges in `database/factories/InventoryLinkFactory.php` -- [ ] T007 Align idempotent upsert semantics for edges in `app/Services/Inventory/DependencyExtractionService.php` -- [ ] T008 Implement warning-only handling for unknown/unsupported reference shapes in `app/Services/Inventory/DependencyExtractionService.php` -- [ ] T009 Persist warnings on the sync run record at `InventorySyncRun.error_context.warnings[]` via `app/Services/Inventory/InventorySyncService.php` -- [ ] T010 Implement limit-only inbound/outbound queries with optional relationship filter in `app/Services/Inventory/DependencyQueryService.php` (ordered by `created_at DESC`) -- [ ] T011 [P] Add determinism + idempotency tests in `tests/Unit/DependencyExtractionServiceTest.php` -- [ ] T012 [P] Add tenant isolation tests for queries in `tests/Feature/DependencyExtractionFeatureTest.php` - [x] T003 [P] Ensure relationship types and labels are centralized in `app/Support/Enums/RelationshipType.php` - [x] T004 Ensure `inventory_links` schema (unique key + indexes) matches spec in `database/migrations/2026_01_07_150000_create_inventory_links_table.php` - [x] T005 [P] Ensure `InventoryLink` casts and tenant-safe query patterns in `app/Models/InventoryLink.php` @@ -50,12 +39,6 @@ ## Phase 3: User Story 1 — View Dependencies (Priority: P1) 🎯 MVP **Goal**: As an admin, I can view direct inbound/outbound dependencies for an inventory item. **Independent Test**: Opening an Inventory Item shows a Dependencies section that renders within limits and supports direction filtering. - -- [ ] T013 [P] [US1] Wire dependencies section into Filament item view in `app/Filament/Resources/InventoryItemResource.php` -- [ ] T014 [P] [US1] Render edges grouped by relationship type in `resources/views/filament/components/dependency-edges.blade.php` -- [ ] T015 [US1] Add direction filter (querystring-backed) in `resources/views/filament/components/dependency-edges.blade.php` -- [ ] T016 [US1] Parse/validate `direction` and pass to query service in `app/Filament/Resources/InventoryItemResource.php` -- [ ] T017 [US1] Add UI smoke test for dependencies rendering + direction filter in `tests/Feature/InventoryItemDependenciesTest.php` - [x] T013 [P] [US1] Wire dependencies section into Filament item view in `app/Filament/Resources/InventoryItemResource.php` - [x] T014 [P] [US1] Render edges grouped by relationship type in `resources/views/filament/components/dependency-edges.blade.php` - [x] T015 [US1] Add direction filter (querystring-backed) in `resources/views/filament/components/dependency-edges.blade.php` @@ -69,11 +52,6 @@ ## Phase 4: User Story 2 — Identify Missing Prerequisites (Priority: P2) **Goal**: As an admin, I can clearly see when a referenced prerequisite object is missing. **Independent Test**: A missing target renders a red “Missing” badge and safe tooltip using `metadata.last_known_name`/`metadata.raw_ref`. - -- [ ] T018 [US2] Ensure unresolved targets create `target_type='missing'` edges with safe metadata in `app/Services/Inventory/DependencyExtractionService.php` -- [ ] T019 [US2] Display “Missing” badge + tooltip for missing edges in `resources/views/filament/components/dependency-edges.blade.php` -- [ ] T020 [US2] Add feature test for missing edge creation + metadata in `tests/Feature/DependencyExtractionFeatureTest.php` -- [ ] T021 [US2] Add UI test asserting missing badge/tooltip is visible in `tests/Feature/InventoryItemDependenciesTest.php` - [x] T018 [US2] Ensure unresolved targets create `target_type='missing'` edges with safe metadata in `app/Services/Inventory/DependencyExtractionService.php` - [x] T019 [US2] Display “Missing” badge + tooltip for missing edges in `resources/views/filament/components/dependency-edges.blade.php` - [x] T020 [US2] Add feature test for missing edge creation + metadata in `tests/Feature/DependencyExtractionFeatureTest.php` @@ -86,10 +64,6 @@ ## Phase 5: User Story 3 — Filter By Relationship Type (Priority: P2) **Goal**: As an admin, I can filter dependencies by relationship type to reduce noise. **Independent Test**: Selecting a relationship type shows only matching edges; default “All” shows everything. - -- [ ] T022 [US3] Add relationship-type dropdown filter (querystring-backed) in `resources/views/filament/components/dependency-edges.blade.php` -- [ ] T023 [US3] Parse/validate `relationship_type` and pass to query service in `app/Filament/Resources/InventoryItemResource.php` -- [ ] T024 [US3] Add UI smoke test verifying relationship filter limits edges in `tests/Feature/InventoryItemDependenciesTest.php` - [x] T022 [US3] Add relationship-type dropdown filter (querystring-backed) in `resources/views/filament/components/dependency-edges.blade.php` - [x] T023 [US3] Parse/validate `relationship_type` and pass to query service in `app/Filament/Resources/InventoryItemResource.php` - [x] T024 [US3] Add UI smoke test verifying relationship filter limits edges in `tests/Feature/InventoryItemDependenciesTest.php` @@ -101,9 +75,6 @@ ## Phase 6: User Story 4 — Zero Dependencies (Priority: P3) **Goal**: As an admin, I get a clear empty state when no dependencies exist. **Independent Test**: When queries return zero edges, the UI shows “No dependencies found” and does not error. - -- [ ] T025 [US4] Render zero-state message in `resources/views/filament/components/dependency-edges.blade.php` -- [ ] T026 [US4] Add UI test for zero-state rendering in `tests/Feature/InventoryItemDependenciesTest.php` - [x] T025 [US4] Render zero-state message in `resources/views/filament/components/dependency-edges.blade.php` - [x] T026 [US4] Add UI test for zero-state rendering in `tests/Feature/InventoryItemDependenciesTest.php` @@ -112,12 +83,6 @@ ## Phase 6: User Story 4 — Zero Dependencies (Priority: P3) ## Phase 7: Polish & Cross-Cutting Concerns **Purpose**: Tighten docs/contracts and run quality gates. - -- [ ] T027 [P] Align filter docs + URLs in `specs/042-inventory-dependencies-graph/quickstart.md` -- [ ] T028 [P] Ensure schema reflects metadata requirements in `specs/042-inventory-dependencies-graph/contracts/dependency-edge.schema.json` -- [ ] T029 Update requirement-gate checkboxes in `specs/042-inventory-dependencies-graph/checklists/pr-gate.md` -- [ ] T030 Run Pint and fix formatting in `app/`, `resources/views/filament/components/`, and `tests/` (touching `app/Support/Enums/RelationshipType.php`, `resources/views/filament/components/dependency-edges.blade.php`, `tests/Feature/InventoryItemDependenciesTest.php`) -- [ ] T031 Run targeted tests: `tests/Feature/InventoryItemDependenciesTest.php`, `tests/Feature/DependencyExtractionFeatureTest.php`, `tests/Unit/DependencyExtractionServiceTest.php` - [x] T027 [P] Align filter docs + URLs in `specs/042-inventory-dependencies-graph/quickstart.md` - [x] T028 [P] Ensure schema reflects metadata requirements in `specs/042-inventory-dependencies-graph/contracts/dependency-edge.schema.json` - [x] T029 Update requirement-gate checkboxes in `specs/042-inventory-dependencies-graph/checklists/pr-gate.md` @@ -129,13 +94,6 @@ ## Phase 7: Polish & Cross-Cutting Concerns ## Phase 8: Consistency & Security Coverage (Cross-Cutting) **Purpose**: Close remaining spec→tasks gaps (ordering, masking, auth expectations, logging severity). - -- [ ] T032 [P] Add test asserting inbound/outbound query ordering by `created_at DESC` + limit-only behavior in `tests/Feature/DependencyExtractionFeatureTest.php` (or `tests/Unit/DependencyExtractionServiceTest.php` if more appropriate) -- [ ] T033 [US2] Implement masked/abbreviated identifier rendering when `metadata.last_known_name` is null in `resources/views/filament/components/dependency-edges.blade.php` -- [ ] T034 [US2] Add UI test for masked identifier behavior in `tests/Feature/InventoryItemDependenciesTest.php` -- [ ] T035 [P] Add auth behavior test for the inventory item dependencies view (guest blocked; authenticated tenant user allowed) in `tests/Feature/InventoryItemDependenciesTest.php` -- [ ] T036 [P] Ensure unknown/unsupported reference warnings are logged at `info` severity in `app/Services/Inventory/DependencyExtractionService.php` and add a unit test using `Log::fake()` in `tests/Unit/DependencyExtractionServiceTest.php` -- [ ] T037 Document how to verify the <2s dependency section goal (manual acceptance check) in `specs/042-inventory-dependencies-graph/quickstart.md` - [x] T032 [P] Add test asserting inbound/outbound query ordering by `created_at DESC` + limit-only behavior in `tests/Feature/DependencyExtractionFeatureTest.php` (or `tests/Unit/DependencyExtractionServiceTest.php` if more appropriate) - [x] T033 [US2] Implement masked/abbreviated identifier rendering when `metadata.last_known_name` is null in `resources/views/filament/components/dependency-edges.blade.php` - [x] T034 [US2] Add UI test for masked identifier behavior in `tests/Feature/InventoryItemDependenciesTest.php` @@ -161,7 +119,6 @@ ## Phase 9: 042.2 — Dependency Target Name Resolution (Resolver + Batch) 🎯 - [x] T042 Add tests: unit (batch + determinism + tenant isolation) and feature/UI (resolved names + external group label) --- - ## Dependencies & Execution Order ### Phase Dependencies