## Summary - collapse secondary and diagnostic operation-run sections by default to reduce page density - visually emphasize the primary next step while keeping counts readable but secondary - keep failures and other actionable detail available without dominating the default reading path ## Testing - vendor/bin/sail artisan test --compact tests/Feature/Filament/OperationRunBaselineTruthSurfaceTest.php tests/Feature/Filament/OperationRunEnterpriseDetailPageTest.php tests/Feature/Filament/EnterpriseDetailTemplateRegressionTest.php tests/Feature/Operations/TenantlessOperationRunViewerTest.php - vendor/bin/sail bin pint --dirty --format agent Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #194
255 lines
9.4 KiB
PHP
255 lines
9.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Livewire;
|
|
|
|
use App\Support\Badges\TagBadgeDomain;
|
|
use App\Support\Badges\TagBadgeRenderer;
|
|
use App\Support\Baselines\BaselineCompareEvidenceGapDetails;
|
|
use App\Support\Filament\TablePaginationProfiles;
|
|
use Filament\Tables\Columns\TextColumn;
|
|
use Filament\Tables\Filters\SelectFilter;
|
|
use Filament\Tables\Table;
|
|
use Filament\Tables\TableComponent;
|
|
use Illuminate\Contracts\View\View;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Str;
|
|
|
|
class BaselineCompareEvidenceGapTable extends TableComponent
|
|
{
|
|
/**
|
|
* @var list<array<string, mixed>>
|
|
*/
|
|
public array $gapRows = [];
|
|
|
|
public string $context = 'default';
|
|
|
|
/**
|
|
* @param list<array<string, mixed>> $buckets
|
|
*/
|
|
public function mount(array $buckets = [], string $context = 'default'): void
|
|
{
|
|
$this->gapRows = BaselineCompareEvidenceGapDetails::tableRows($buckets);
|
|
$this->context = $context;
|
|
}
|
|
|
|
public function table(Table $table): Table
|
|
{
|
|
return $table
|
|
->queryStringIdentifier('baselineCompareEvidenceGap'.Str::studly($this->context))
|
|
->defaultSort('reason_label')
|
|
->defaultPaginationPageOption(10)
|
|
->paginated(TablePaginationProfiles::picker())
|
|
->searchable()
|
|
->searchPlaceholder(__('baseline-compare.evidence_gap_search_placeholder'))
|
|
->records(function (
|
|
?string $sortColumn,
|
|
?string $sortDirection,
|
|
?string $search,
|
|
array $filters,
|
|
int $page,
|
|
int $recordsPerPage
|
|
): LengthAwarePaginator {
|
|
$rows = $this->filterRows(
|
|
rows: collect($this->gapRows),
|
|
search: $search,
|
|
filters: $filters,
|
|
);
|
|
|
|
$rows = $this->sortRows(
|
|
rows: $rows,
|
|
sortColumn: $sortColumn,
|
|
sortDirection: $sortDirection,
|
|
);
|
|
|
|
return $this->paginateRows(
|
|
rows: $rows,
|
|
page: $page,
|
|
recordsPerPage: $recordsPerPage,
|
|
);
|
|
})
|
|
->filters([
|
|
SelectFilter::make('reason_code')
|
|
->label(__('baseline-compare.evidence_gap_reason'))
|
|
->options(BaselineCompareEvidenceGapDetails::reasonFilterOptions($this->gapRows)),
|
|
SelectFilter::make('policy_type')
|
|
->label(__('baseline-compare.evidence_gap_policy_type'))
|
|
->options(BaselineCompareEvidenceGapDetails::policyTypeFilterOptions($this->gapRows)),
|
|
SelectFilter::make('subject_class')
|
|
->label(__('baseline-compare.evidence_gap_subject_class'))
|
|
->options(BaselineCompareEvidenceGapDetails::subjectClassFilterOptions($this->gapRows)),
|
|
SelectFilter::make('operator_action_category')
|
|
->label(__('baseline-compare.evidence_gap_next_action'))
|
|
->options(BaselineCompareEvidenceGapDetails::actionCategoryFilterOptions($this->gapRows)),
|
|
])
|
|
->striped()
|
|
->deferLoading(! app()->runningUnitTests())
|
|
->columns([
|
|
TextColumn::make('reason_label')
|
|
->label(__('baseline-compare.evidence_gap_reason'))
|
|
->searchable()
|
|
->sortable()
|
|
->wrap()
|
|
->toggleable(isToggledHiddenByDefault: true),
|
|
TextColumn::make('policy_type')
|
|
->label(__('baseline-compare.evidence_gap_policy_type'))
|
|
->badge()
|
|
->formatStateUsing(TagBadgeRenderer::label(TagBadgeDomain::PolicyType))
|
|
->color(TagBadgeRenderer::color(TagBadgeDomain::PolicyType))
|
|
->icon(TagBadgeRenderer::icon(TagBadgeDomain::PolicyType))
|
|
->iconColor(TagBadgeRenderer::iconColor(TagBadgeDomain::PolicyType))
|
|
->searchable()
|
|
->sortable()
|
|
->wrap(),
|
|
TextColumn::make('subject_class_label')
|
|
->label(__('baseline-compare.evidence_gap_subject_class'))
|
|
->badge()
|
|
->searchable()
|
|
->sortable()
|
|
->wrap(),
|
|
TextColumn::make('resolution_outcome_label')
|
|
->label(__('baseline-compare.evidence_gap_outcome'))
|
|
->searchable()
|
|
->sortable()
|
|
->wrap(),
|
|
TextColumn::make('operator_action_category_label')
|
|
->label(__('baseline-compare.evidence_gap_next_action'))
|
|
->searchable()
|
|
->sortable()
|
|
->wrap(),
|
|
TextColumn::make('subject_key')
|
|
->label(__('baseline-compare.evidence_gap_subject_key'))
|
|
->searchable()
|
|
->sortable()
|
|
->wrap(),
|
|
])
|
|
->actions([])
|
|
->bulkActions([])
|
|
->emptyStateHeading(__('baseline-compare.evidence_gap_table_empty_heading'))
|
|
->emptyStateDescription(__('baseline-compare.evidence_gap_table_empty_description'));
|
|
}
|
|
|
|
public function render(): View
|
|
{
|
|
return view('livewire.baseline-compare-evidence-gap-table');
|
|
}
|
|
|
|
/**
|
|
* @param Collection<int, array<string, mixed>> $rows
|
|
* @param array<string, mixed> $filters
|
|
* @return Collection<int, array<string, mixed>>
|
|
*/
|
|
private function filterRows(Collection $rows, ?string $search, array $filters): Collection
|
|
{
|
|
$normalizedSearch = Str::lower(trim((string) $search));
|
|
$reasonCode = $filters['reason_code']['value'] ?? null;
|
|
$policyType = $filters['policy_type']['value'] ?? null;
|
|
$subjectClass = $filters['subject_class']['value'] ?? null;
|
|
$operatorActionCategory = $filters['operator_action_category']['value'] ?? null;
|
|
|
|
return $rows
|
|
->when(
|
|
$normalizedSearch !== '',
|
|
function (Collection $rows) use ($normalizedSearch): Collection {
|
|
return $rows->filter(function (array $row) use ($normalizedSearch): bool {
|
|
return str_contains(Str::lower((string) ($row['search_text'] ?? '')), $normalizedSearch);
|
|
});
|
|
}
|
|
)
|
|
->when(
|
|
filled($reasonCode),
|
|
fn (Collection $rows): Collection => $rows->where('reason_code', (string) $reasonCode)
|
|
)
|
|
->when(
|
|
filled($policyType),
|
|
fn (Collection $rows): Collection => $rows->where('policy_type', (string) $policyType)
|
|
)
|
|
->when(
|
|
filled($subjectClass),
|
|
fn (Collection $rows): Collection => $rows->where('subject_class', (string) $subjectClass)
|
|
)
|
|
->when(
|
|
filled($operatorActionCategory),
|
|
fn (Collection $rows): Collection => $rows->where('operator_action_category', (string) $operatorActionCategory)
|
|
)
|
|
->values();
|
|
}
|
|
|
|
/**
|
|
* @param Collection<int, array<string, mixed>> $rows
|
|
* @return Collection<int, array<string, mixed>>
|
|
*/
|
|
private function sortRows(Collection $rows, ?string $sortColumn, ?string $sortDirection): Collection
|
|
{
|
|
if (! filled($sortColumn)) {
|
|
return $rows;
|
|
}
|
|
|
|
$direction = Str::lower((string) ($sortDirection ?? 'asc')) === 'desc' ? 'desc' : 'asc';
|
|
|
|
return $rows->sortBy(
|
|
fn (array $row): string => (string) ($row[$sortColumn] ?? ''),
|
|
SORT_NATURAL | SORT_FLAG_CASE,
|
|
$direction === 'desc'
|
|
)->values();
|
|
}
|
|
|
|
/**
|
|
* @param Collection<int, array<string, mixed>> $rows
|
|
*/
|
|
private function paginateRows(Collection $rows, int $page, int $recordsPerPage): LengthAwarePaginator
|
|
{
|
|
$perPage = max(1, $recordsPerPage);
|
|
$currentPage = max(1, $page);
|
|
$total = $rows->count();
|
|
$items = $rows->forPage($currentPage, $perPage)
|
|
->values()
|
|
->map(fn (array $row, int $index): Model => $this->toTableRecord(
|
|
row: $row,
|
|
index: (($currentPage - 1) * $perPage) + $index,
|
|
));
|
|
|
|
return new LengthAwarePaginator(
|
|
$items,
|
|
$total,
|
|
$perPage,
|
|
$currentPage,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $row
|
|
*/
|
|
private function toTableRecord(array $row, int $index): Model
|
|
{
|
|
$record = new class extends Model
|
|
{
|
|
public $timestamps = false;
|
|
|
|
public $incrementing = false;
|
|
|
|
protected $keyType = 'string';
|
|
|
|
protected $guarded = [];
|
|
|
|
protected $table = 'baseline_compare_evidence_gap_rows';
|
|
};
|
|
|
|
$record->forceFill([
|
|
'id' => implode(':', array_filter([
|
|
(string) ($row['reason_code'] ?? 'reason'),
|
|
(string) ($row['policy_type'] ?? 'policy'),
|
|
(string) ($row['subject_key'] ?? 'subject'),
|
|
(string) $index,
|
|
])),
|
|
...$row,
|
|
]);
|
|
$record->exists = true;
|
|
|
|
return $record;
|
|
}
|
|
}
|