inventoryItemId = $inventoryItemId; $this->resolveInventoryItem(); } public function table(Table $table): Table { return $table ->queryStringIdentifier('inventoryItemDependencyEdges'.Str::studly((string) $this->inventoryItemId)) ->defaultSort('relationship_label') ->defaultPaginationPageOption(10) ->paginated(TablePaginationProfiles::picker()) ->striped() ->deferLoading(! app()->runningUnitTests()) ->records(function ( ?string $sortColumn, ?string $sortDirection, ?string $search, array $filters, int $page, int $recordsPerPage ): LengthAwarePaginator { $rows = $this->dependencyRows( direction: (string) ($filters['direction']['value'] ?? 'all'), relationshipType: $this->normalizeRelationshipType($filters['relationship_type']['value'] ?? null), ); $rows = $this->sortRows($rows, $sortColumn, $sortDirection); return $this->paginateRows($rows, $page, $recordsPerPage); }) ->filters([ SelectFilter::make('direction') ->label('Direction') ->default('all') ->options([ 'all' => 'All', 'inbound' => 'Inbound', 'outbound' => 'Outbound', ]), SelectFilter::make('relationship_type') ->label('Relationship') ->options([ 'all' => 'All', ...RelationshipType::options(), ]) ->default('all') ->searchable(), ]) ->columns([ TextColumn::make('relationship_label') ->label('Relationship') ->badge() ->sortable(), TextColumn::make('target_label') ->label('Target') ->badge() ->url(fn (array $record): ?string => is_string($record['target_url'] ?? null) ? $record['target_url'] : null) ->tooltip(fn (array $record): ?string => is_string($record['target_tooltip'] ?? null) ? $record['target_tooltip'] : null) ->wrap(), TextColumn::make('missing_state') ->label('Status') ->badge() ->placeholder('—') ->color(fn (?string $state): string => $state === 'Missing' ? 'danger' : 'gray') ->icon(fn (?string $state): ?string => $state === 'Missing' ? 'heroicon-m-exclamation-triangle' : null) ->description(fn (array $record): ?string => is_string($record['missing_hint'] ?? null) ? $record['missing_hint'] : null) ->wrap(), ]) ->actions([]) ->bulkActions([]) ->emptyStateHeading('No dependencies found') ->emptyStateDescription('Change direction or relationship filters to review a different dependency scope.'); } public function render(): View { return view('livewire.inventory-item-dependency-edges-table'); } /** * @return Collection> */ private function dependencyRows(string $direction, ?string $relationshipType): Collection { $inventoryItem = $this->resolveInventoryItem(); $tenant = $this->resolveCurrentTenant(); $service = app(DependencyQueryService::class); $resolver = app(DependencyTargetResolver::class); $edges = collect(); if ($direction === 'inbound' || $direction === 'all') { $edges = $edges->merge($service->getInboundEdges($inventoryItem, $relationshipType)); } if ($direction === 'outbound' || $direction === 'all') { $edges = $edges->merge($service->getOutboundEdges($inventoryItem, $relationshipType)); } return $resolver->attachRenderedTargets($edges->take(100), $tenant) ->map(function (array $edge): array { $targetId = $edge['target_id'] ?? null; $renderedTarget = is_array($edge['rendered_target'] ?? null) ? $edge['rendered_target'] : []; $badgeText = is_string($renderedTarget['badge_text'] ?? null) ? $renderedTarget['badge_text'] : null; $linkUrl = is_string($renderedTarget['link_url'] ?? null) ? $renderedTarget['link_url'] : null; $lastKnownName = is_string(data_get($edge, 'metadata.last_known_name')) ? data_get($edge, 'metadata.last_known_name') : null; $isMissing = ($edge['target_type'] ?? null) === 'missing'; $missingHint = null; if ($isMissing) { $missingHint = 'Missing target'; if (filled($lastKnownName)) { $missingHint .= ". Last known: {$lastKnownName}"; } $rawRef = data_get($edge, 'metadata.raw_ref'); $encodedRef = $rawRef !== null ? json_encode($rawRef) : null; if (is_string($encodedRef) && $encodedRef !== '') { $missingHint .= '. Ref: '.Str::limit($encodedRef, 200); } } $fallbackLabel = null; if (filled($lastKnownName)) { $fallbackLabel = $lastKnownName; } elseif (is_string($targetId) && $targetId !== '') { $fallbackLabel = 'ID: '.substr($targetId, 0, 6).'…'; } else { $fallbackLabel = 'External reference'; } $relationshipType = (string) ($edge['relationship_type'] ?? 'unknown'); return [ 'id' => (string) ($edge['id'] ?? Str::uuid()->toString()), 'relationship_label' => RelationshipType::options()[$relationshipType] ?? Str::headline($relationshipType), 'target_label' => $badgeText ?? $fallbackLabel, 'target_url' => $linkUrl, 'target_tooltip' => is_string($targetId) ? $targetId : null, 'missing_state' => $isMissing ? 'Missing' : null, 'missing_hint' => $missingHint, ]; }) ->mapWithKeys(fn (array $row): array => [$row['id'] => $row]); } /** * @param Collection> $rows * @return Collection> */ private function sortRows(Collection $rows, ?string $sortColumn, ?string $sortDirection): Collection { $sortColumn = in_array($sortColumn, ['relationship_label', 'target_label', 'missing_state'], true) ? $sortColumn : 'relationship_label'; $descending = Str::lower((string) ($sortDirection ?? 'asc')) === 'desc'; $records = $rows->all(); uasort($records, static function (array $left, array $right) use ($sortColumn, $descending): int { $comparison = strnatcasecmp( (string) ($left[$sortColumn] ?? ''), (string) ($right[$sortColumn] ?? ''), ); if ($comparison === 0) { $comparison = strnatcasecmp( (string) ($left['target_label'] ?? ''), (string) ($right['target_label'] ?? ''), ); } return $descending ? ($comparison * -1) : $comparison; }); return collect($records); } /** * @param Collection> $rows */ private function paginateRows(Collection $rows, int $page, int $recordsPerPage): LengthAwarePaginator { return new LengthAwarePaginator( items: $rows->forPage($page, $recordsPerPage), total: $rows->count(), perPage: $recordsPerPage, currentPage: $page, ); } private function resolveInventoryItem(): InventoryItem { if ($this->cachedInventoryItem instanceof InventoryItem) { return $this->cachedInventoryItem; } $inventoryItem = InventoryItem::query()->findOrFail($this->inventoryItemId); $tenant = $this->resolveCurrentTenant(); if ((int) $inventoryItem->tenant_id !== (int) $tenant->getKey() || ! InventoryItemResource::canView($inventoryItem)) { throw new NotFoundHttpException; } return $this->cachedInventoryItem = $inventoryItem; } private function resolveCurrentTenant(): Tenant { $tenant = Filament::getTenant(); if (! $tenant instanceof Tenant) { throw new NotFoundHttpException; } return $tenant; } private function normalizeRelationshipType(mixed $value): ?string { if (! is_string($value) || $value === '' || $value === 'all') { return null; } return RelationshipType::tryFrom($value)?->value; } }