Dieses PR liefert den Inventory Dependencies Graph end-to-end: Abhängigkeiten (Edges) werden aus Inventory-Sync-Daten extrahiert, tenant-sicher gespeichert und in der Inventory Item Detailansicht angezeigt. Ziel: Admins können Prerequisites + Blast Radius (direct) schnell erkennen, ohne Snapshot/Restore anzufassen. ⸻ Was ist drin? Dependency Graph (Edges) • inventory_links Schema + Indizes + idempotentes Upsert (Unique Key) • Relationship Types (u.a.): • assigned_to_include, assigned_to_exclude • uses_assignment_filter • scoped_by_scope_tag • UI: Inventory Item → Dependencies Section • Direction Filter: All / Inbound / Outbound • Relationship Filter: All + spezifische Relationship Types • Missing-Badge + sicheres Tooltip (safe subset) Safety / Observability • Unknown/unsupported Shapes erzeugen keine Edges, sondern: • Warning in InventorySyncRun.error_context.warnings[] • optional info-log (ohne Secrets) • Limit-only Semantik (MVP): bis zu 50 Edges pro Richtung (max 100 bei “All”) • Blast Radius in MVP = direct only (kein depth>1 traversal) Name Resolution (lokal, ohne Entra Calls) • Resolver/DTO Layer für deterministische Labels (kein “Unknown” mehr) • Auflösung aus lokaler DB nur für Foundations, wenn vorhanden: • scope_tag → roleScopeTag • assignment_filter → assignmentFilter • aad_group bleibt bewusst external ref: “Group (external): …” (keine Graph/Entra Lookups im UI) • Zentraler FoundationTypeMap als Source-of-Truth (keine Hardcodings) ⸻ Out of Scope / Follow-up • Entra Group Name Resolution (braucht eigenes “Group Inventory” Modul + Permissions) • Foundations als Inventory Items / Coverage Tab (Scope Tags / Assignment Filters sichtbar & syncbar) → folgt als separater PR (Inventory Core/UI), damit 042 sauber “Edges-only” bleibt. ⸻ Tests / Verifikation • Targeted Pest Tests (Unit + Feature + UI smoke) für: • deterministische Edge-Erzeugung + idempotent upsert • tenant isolation (UI/Query) • warnings auf Run Record • resolver/name rendering + links (wo möglich) • pint --dirty ausgeführt ⸻ Manual QA (UI) 1. Inventory Sync Run mit include_dependencies=true starten 2. Inventory Item öffnen → Dependencies prüfen: • include/exclude + filter + scoped_by sichtbar (wenn vorhanden) • Relationship/Direction Filter funktionieren • keine “Unknown” Labels mehr, sondern deterministische Labels Co-authored-by: Ahmed Darrazi <ahmeddarrazi@adsmac.local> Reviewed-on: #50
86 lines
4.9 KiB
PHP
86 lines
4.9 KiB
PHP
@php /** @var callable $getState */ @endphp
|
|
|
|
<div class="space-y-4">
|
|
<form method="GET" class="flex items-center gap-2">
|
|
<label for="direction" class="text-sm text-gray-600">Direction</label>
|
|
<select id="direction" name="direction" class="fi-input fi-select">
|
|
<option value="all" {{ request('direction', 'all') === 'all' ? 'selected' : '' }}>All</option>
|
|
<option value="inbound" {{ request('direction') === 'inbound' ? 'selected' : '' }}>Inbound</option>
|
|
<option value="outbound" {{ request('direction') === 'outbound' ? 'selected' : '' }}>Outbound</option>
|
|
</select>
|
|
|
|
<label for="relationship_type" class="text-sm text-gray-600">Relationship</label>
|
|
<select id="relationship_type" name="relationship_type" class="fi-input fi-select">
|
|
<option value="all" {{ request('relationship_type', 'all') === 'all' ? 'selected' : '' }}>All</option>
|
|
@foreach (\App\Support\Enums\RelationshipType::options() as $value => $label)
|
|
<option value="{{ $value }}" {{ request('relationship_type') === $value ? 'selected' : '' }}>{{ $label }}</option>
|
|
@endforeach
|
|
</select>
|
|
<button type="submit" class="fi-btn">Apply</button>
|
|
</form>
|
|
|
|
@php
|
|
$raw = $getState();
|
|
$edges = $raw instanceof \Illuminate\Support\Collection ? $raw : collect($raw);
|
|
@endphp
|
|
|
|
@if ($edges->isEmpty())
|
|
<div class="text-sm text-gray-500">No dependencies found</div>
|
|
@else
|
|
<div class="divide-y">
|
|
@foreach ($edges->groupBy('relationship_type') as $type => $group)
|
|
<div class="py-2">
|
|
<div class="text-xs uppercase tracking-wide text-gray-600 mb-2">{{ str_replace('_', ' ', $type) }}</div>
|
|
<ul class="space-y-1">
|
|
@foreach ($group as $edge)
|
|
@php
|
|
$isMissing = ($edge['target_type'] ?? null) === 'missing';
|
|
$targetId = $edge['target_id'] ?? null;
|
|
$rendered = $edge['rendered_target'] ?? [];
|
|
$badgeText = is_array($rendered) ? ($rendered['badge_text'] ?? null) : null;
|
|
$linkUrl = is_array($rendered) ? ($rendered['link_url'] ?? null) : null;
|
|
|
|
$missingTitle = 'Missing target';
|
|
$lastKnownName = $edge['metadata']['last_known_name'] ?? null;
|
|
if (is_string($lastKnownName) && $lastKnownName !== '') {
|
|
$missingTitle .= ". Last known: {$lastKnownName}";
|
|
}
|
|
$rawRef = $edge['metadata']['raw_ref'] ?? null;
|
|
if ($rawRef !== null) {
|
|
$encodedRef = json_encode($rawRef);
|
|
if (is_string($encodedRef) && $encodedRef !== '') {
|
|
$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
|
|
<li class="flex items-center gap-2 text-sm">
|
|
@if (is_string($badgeText) && $badgeText !== '')
|
|
@if (is_string($linkUrl) && $linkUrl !== '')
|
|
<a class="fi-badge" href="{{ $linkUrl }}" title="{{ is_string($targetId) ? $targetId : '' }}">{{ $badgeText }}</a>
|
|
@else
|
|
<span class="fi-badge" title="{{ is_string($targetId) ? $targetId : '' }}">{{ $badgeText }}</span>
|
|
@endif
|
|
@else
|
|
<span class="fi-badge" title="{{ is_string($targetId) ? $targetId : '' }}">{{ $fallbackDisplay }}</span>
|
|
@endif
|
|
@if ($isMissing)
|
|
<span class="fi-badge fi-badge-danger" title="{{ $missingTitle }}">Missing</span>
|
|
@endif
|
|
</li>
|
|
@endforeach
|
|
</ul>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
</div>
|