Implements Spec 075 (V1.5) on top of Spec 074. Highlights - Deterministic report fingerprint (sha256) + previous_report_id linkage - Viewer change indicator: "No changes" vs "Changed" when previous exists - Check acknowledgements (fail|warn|block) with capability-first auth, confirmation, and audit event - Verify-step UX polish (issues-first, primary CTA) Testing - Focused Pest coverage for fingerprint, previous resolver, change indicator, acknowledgements, badge semantics, DB-only viewer guard. Notes - Viewing remains DB-only (no external calls while rendering). Co-authored-by: Ahmed Darrazi <ahmeddarrazi@MacBookPro.fritz.box> Reviewed-on: #93
597 lines
36 KiB
PHP
597 lines
36 KiB
PHP
@php
|
||
$fieldWrapperView = $getFieldWrapperView();
|
||
|
||
$run = $run ?? null;
|
||
$run = is_array($run) ? $run : null;
|
||
|
||
$runUrl = $runUrl ?? null;
|
||
$runUrl = is_string($runUrl) && $runUrl !== '' ? $runUrl : null;
|
||
|
||
$report = $report ?? null;
|
||
$report = is_array($report) ? $report : null;
|
||
|
||
$fingerprint = $fingerprint ?? null;
|
||
$fingerprint = is_string($fingerprint) && trim($fingerprint) !== '' ? trim($fingerprint) : null;
|
||
|
||
$changeIndicator = $changeIndicator ?? null;
|
||
$changeIndicator = is_array($changeIndicator) ? $changeIndicator : null;
|
||
|
||
$previousRunUrl = $previousRunUrl ?? null;
|
||
$previousRunUrl = is_string($previousRunUrl) && $previousRunUrl !== '' ? $previousRunUrl : null;
|
||
|
||
$canAcknowledge = (bool) ($canAcknowledge ?? false);
|
||
|
||
$acknowledgements = $acknowledgements ?? [];
|
||
$acknowledgements = is_array($acknowledgements) ? $acknowledgements : [];
|
||
|
||
$status = $run['status'] ?? null;
|
||
$status = is_string($status) ? $status : null;
|
||
|
||
$outcome = $run['outcome'] ?? null;
|
||
$outcome = is_string($outcome) ? $outcome : null;
|
||
|
||
$targetScope = $run['target_scope'] ?? [];
|
||
$targetScope = is_array($targetScope) ? $targetScope : [];
|
||
|
||
$failures = $run['failures'] ?? [];
|
||
$failures = is_array($failures) ? $failures : [];
|
||
|
||
$completedAt = $run['completed_at'] ?? null;
|
||
$completedAt = is_string($completedAt) && $completedAt !== '' ? $completedAt : null;
|
||
|
||
$completedAtLabel = null;
|
||
|
||
if ($completedAt !== null) {
|
||
try {
|
||
$completedAtLabel = \Carbon\CarbonImmutable::parse($completedAt)->format('Y-m-d H:i');
|
||
} catch (\Throwable) {
|
||
$completedAtLabel = $completedAt;
|
||
}
|
||
}
|
||
|
||
$summary = $report['summary'] ?? null;
|
||
$summary = is_array($summary) ? $summary : null;
|
||
|
||
$counts = is_array($summary['counts'] ?? null) ? $summary['counts'] : [];
|
||
|
||
$checks = $report['checks'] ?? null;
|
||
$checks = is_array($checks) ? $checks : [];
|
||
|
||
$ackByKey = [];
|
||
|
||
foreach ($acknowledgements as $checkKey => $ack) {
|
||
if (! is_string($checkKey) || $checkKey === '' || ! is_array($ack)) {
|
||
continue;
|
||
}
|
||
|
||
$ackByKey[$checkKey] = $ack;
|
||
}
|
||
|
||
$blockers = [];
|
||
$failures = [];
|
||
$warnings = [];
|
||
$acknowledgedIssues = [];
|
||
$passed = [];
|
||
|
||
foreach ($checks as $check) {
|
||
$check = is_array($check) ? $check : [];
|
||
|
||
$key = $check['key'] ?? null;
|
||
$key = is_string($key) ? trim($key) : '';
|
||
|
||
if ($key === '') {
|
||
continue;
|
||
}
|
||
|
||
$statusValue = $check['status'] ?? null;
|
||
$statusValue = is_string($statusValue) ? strtolower(trim($statusValue)) : '';
|
||
|
||
$blocking = $check['blocking'] ?? false;
|
||
$blocking = is_bool($blocking) ? $blocking : false;
|
||
|
||
if (array_key_exists($key, $ackByKey)) {
|
||
$acknowledgedIssues[] = $check;
|
||
continue;
|
||
}
|
||
|
||
if ($statusValue === 'pass') {
|
||
$passed[] = $check;
|
||
continue;
|
||
}
|
||
|
||
if ($statusValue === 'fail' && $blocking) {
|
||
$blockers[] = $check;
|
||
continue;
|
||
}
|
||
|
||
if ($statusValue === 'fail') {
|
||
$failures[] = $check;
|
||
continue;
|
||
}
|
||
|
||
if ($statusValue === 'warn') {
|
||
$warnings[] = $check;
|
||
}
|
||
}
|
||
|
||
$sortChecks = static function (array $a, array $b): int {
|
||
return strcmp((string) ($a['key'] ?? ''), (string) ($b['key'] ?? ''));
|
||
};
|
||
|
||
usort($blockers, $sortChecks);
|
||
usort($failures, $sortChecks);
|
||
usort($warnings, $sortChecks);
|
||
usort($acknowledgedIssues, $sortChecks);
|
||
usort($passed, $sortChecks);
|
||
|
||
$ackAction = null;
|
||
|
||
if (isset($this) && method_exists($this, 'acknowledgeVerificationCheckAction')) {
|
||
$ackAction = $this->acknowledgeVerificationCheckAction();
|
||
}
|
||
@endphp
|
||
|
||
<x-dynamic-component :component="$fieldWrapperView" :field="$field">
|
||
<div class="space-y-4">
|
||
<x-filament::section
|
||
heading="Verification report"
|
||
:description="$completedAtLabel ? ('Completed: ' . $completedAtLabel) : 'Stored details for the latest verification run.'"
|
||
>
|
||
@if ($run === null)
|
||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||
No verification run has been started yet.
|
||
</div>
|
||
@elseif ($status !== 'completed')
|
||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||
Report unavailable while the run is in progress. Use “Refresh” to re-check stored status.
|
||
</div>
|
||
@else
|
||
<div class="space-y-4">
|
||
<div class="rounded-lg border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||
@php
|
||
$overallSpec = $summary === null
|
||
? null
|
||
: \App\Support\Badges\BadgeRenderer::spec(
|
||
\App\Support\Badges\BadgeDomain::VerificationReportOverall,
|
||
$summary['overall'] ?? null,
|
||
);
|
||
@endphp
|
||
|
||
<div class="flex flex-wrap items-center gap-2">
|
||
@if ($overallSpec)
|
||
<x-filament::badge :color="$overallSpec->color" :icon="$overallSpec->icon">
|
||
{{ $overallSpec->label }}
|
||
</x-filament::badge>
|
||
@endif
|
||
|
||
<x-filament::badge color="gray">
|
||
{{ (int) ($counts['total'] ?? 0) }} total
|
||
</x-filament::badge>
|
||
<x-filament::badge color="success">
|
||
{{ (int) ($counts['pass'] ?? 0) }} pass
|
||
</x-filament::badge>
|
||
<x-filament::badge color="danger">
|
||
{{ (int) ($counts['fail'] ?? 0) }} fail
|
||
</x-filament::badge>
|
||
<x-filament::badge color="warning">
|
||
{{ (int) ($counts['warn'] ?? 0) }} warn
|
||
</x-filament::badge>
|
||
<x-filament::badge color="gray">
|
||
{{ (int) ($counts['skip'] ?? 0) }} skip
|
||
</x-filament::badge>
|
||
<x-filament::badge color="info">
|
||
{{ (int) ($counts['running'] ?? 0) }} running
|
||
</x-filament::badge>
|
||
|
||
@if ($changeIndicator !== null)
|
||
@php
|
||
$state = $changeIndicator['state'] ?? null;
|
||
$state = is_string($state) ? $state : null;
|
||
@endphp
|
||
|
||
@if ($state === 'no_changes')
|
||
<x-filament::badge color="success">
|
||
No changes since previous verification
|
||
</x-filament::badge>
|
||
@elseif ($state === 'changed')
|
||
<x-filament::badge color="warning">
|
||
Changed since previous verification
|
||
</x-filament::badge>
|
||
@endif
|
||
@endif
|
||
</div>
|
||
|
||
<div class="mt-2 text-xs text-gray-600 dark:text-gray-300">
|
||
<span class="font-semibold">Read-only:</span> this view uses stored data and makes no external calls.
|
||
</div>
|
||
</div>
|
||
|
||
@if ($report === null || $summary === null)
|
||
<div class="rounded-lg border border-gray-200 bg-white p-4 text-sm text-gray-600 shadow-sm dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300">
|
||
<div class="font-medium text-gray-900 dark:text-white">
|
||
Verification report unavailable
|
||
</div>
|
||
<div class="mt-1">
|
||
This run doesn’t have a report yet. If it already completed, start verification again.
|
||
</div>
|
||
</div>
|
||
@else
|
||
<div
|
||
x-data="{ tab: 'issues' }"
|
||
class="space-y-4"
|
||
>
|
||
<x-filament::tabs label="Verification report tabs">
|
||
<x-filament::tabs.item
|
||
:active="true"
|
||
alpine-active="tab === 'issues'"
|
||
x-on:click="tab = 'issues'"
|
||
>
|
||
Issues
|
||
</x-filament::tabs.item>
|
||
<x-filament::tabs.item
|
||
:active="false"
|
||
alpine-active="tab === 'passed'"
|
||
x-on:click="tab = 'passed'"
|
||
>
|
||
Passed
|
||
</x-filament::tabs.item>
|
||
<x-filament::tabs.item
|
||
:active="false"
|
||
alpine-active="tab === 'technical'"
|
||
x-on:click="tab = 'technical'"
|
||
>
|
||
Technical details
|
||
</x-filament::tabs.item>
|
||
</x-filament::tabs>
|
||
|
||
<div x-show="tab === 'issues'">
|
||
@if ($blockers === [] && $failures === [] && $warnings === [] && $acknowledgedIssues === [])
|
||
<div class="text-sm text-gray-700 dark:text-gray-200">
|
||
No issues found in this report.
|
||
</div>
|
||
@else
|
||
<div class="space-y-3">
|
||
@php
|
||
$issueGroups = [
|
||
['label' => 'Blockers', 'checks' => $blockers],
|
||
['label' => 'Failures', 'checks' => $failures],
|
||
['label' => 'Warnings', 'checks' => $warnings],
|
||
];
|
||
@endphp
|
||
|
||
@foreach ($issueGroups as $group)
|
||
@php
|
||
$label = $group['label'];
|
||
$groupChecks = $group['checks'];
|
||
@endphp
|
||
|
||
@if ($groupChecks !== [])
|
||
<div class="space-y-2">
|
||
<div class="text-sm font-semibold text-gray-900 dark:text-white">
|
||
{{ $label }}
|
||
</div>
|
||
|
||
<div class="space-y-2">
|
||
@foreach ($groupChecks as $check)
|
||
@php
|
||
$check = is_array($check) ? $check : [];
|
||
$checkKey = is_string($check['key'] ?? null) ? trim((string) $check['key']) : '';
|
||
|
||
$title = $check['title'] ?? 'Check';
|
||
$title = is_string($title) && trim($title) !== '' ? trim($title) : 'Check';
|
||
|
||
$message = $check['message'] ?? null;
|
||
$message = is_string($message) && trim($message) !== '' ? trim($message) : null;
|
||
|
||
$statusSpec = \App\Support\Badges\BadgeRenderer::spec(
|
||
\App\Support\Badges\BadgeDomain::VerificationCheckStatus,
|
||
$check['status'] ?? null,
|
||
);
|
||
|
||
$severitySpec = \App\Support\Badges\BadgeRenderer::spec(
|
||
\App\Support\Badges\BadgeDomain::VerificationCheckSeverity,
|
||
$check['severity'] ?? null,
|
||
);
|
||
|
||
$nextSteps = $check['next_steps'] ?? [];
|
||
$nextSteps = is_array($nextSteps) ? array_slice($nextSteps, 0, 2) : [];
|
||
|
||
$blocking = $check['blocking'] ?? false;
|
||
$blocking = is_bool($blocking) ? $blocking : false;
|
||
@endphp
|
||
|
||
<div class="rounded-lg border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||
<div class="space-y-1">
|
||
<div class="text-sm font-medium text-gray-900 dark:text-white">
|
||
{{ $title }}
|
||
</div>
|
||
@if ($message)
|
||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||
{{ $message }}
|
||
</div>
|
||
@endif
|
||
</div>
|
||
|
||
<div class="flex shrink-0 flex-wrap items-center justify-end gap-2">
|
||
@if ($blocking)
|
||
<x-filament::badge color="danger" size="sm">
|
||
Blocker
|
||
</x-filament::badge>
|
||
@endif
|
||
|
||
<x-filament::badge :color="$severitySpec->color" :icon="$severitySpec->icon" size="sm">
|
||
{{ $severitySpec->label }}
|
||
</x-filament::badge>
|
||
<x-filament::badge :color="$statusSpec->color" :icon="$statusSpec->icon" size="sm">
|
||
{{ $statusSpec->label }}
|
||
</x-filament::badge>
|
||
|
||
@if ($ackAction !== null && $canAcknowledge && $checkKey !== '')
|
||
{{ ($ackAction)(['check_key' => $checkKey]) }}
|
||
@endif
|
||
</div>
|
||
</div>
|
||
|
||
@if ($nextSteps !== [])
|
||
<div class="mt-4">
|
||
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||
Next steps
|
||
</div>
|
||
<ul class="mt-2 space-y-1 text-sm">
|
||
@foreach ($nextSteps as $step)
|
||
@php
|
||
$step = is_array($step) ? $step : [];
|
||
$label = $step['label'] ?? null;
|
||
$url = $step['url'] ?? null;
|
||
$isExternal = is_string($url) && (str_starts_with($url, 'http://') || str_starts_with($url, 'https://'));
|
||
@endphp
|
||
|
||
@if (is_string($label) && $label !== '' && is_string($url) && $url !== '')
|
||
<li>
|
||
<a
|
||
href="{{ $url }}"
|
||
class="text-primary-600 hover:underline dark:text-primary-400"
|
||
@if ($isExternal)
|
||
target="_blank" rel="noreferrer"
|
||
@endif
|
||
>
|
||
{{ $label }}
|
||
</a>
|
||
</li>
|
||
@endif
|
||
@endforeach
|
||
</ul>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
</div>
|
||
@endif
|
||
@endforeach
|
||
|
||
@if ($acknowledgedIssues !== [])
|
||
<details class="rounded-lg border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||
<summary class="cursor-pointer text-sm font-semibold text-gray-900 dark:text-white">
|
||
Acknowledged issues
|
||
</summary>
|
||
|
||
<div class="mt-4 space-y-2">
|
||
@foreach ($acknowledgedIssues as $check)
|
||
@php
|
||
$check = is_array($check) ? $check : [];
|
||
$checkKey = is_string($check['key'] ?? null) ? trim((string) $check['key']) : '';
|
||
|
||
$title = $check['title'] ?? 'Check';
|
||
$title = is_string($title) && trim($title) !== '' ? trim($title) : 'Check';
|
||
|
||
$message = $check['message'] ?? null;
|
||
$message = is_string($message) && trim($message) !== '' ? trim($message) : null;
|
||
|
||
$statusSpec = \App\Support\Badges\BadgeRenderer::spec(
|
||
\App\Support\Badges\BadgeDomain::VerificationCheckStatus,
|
||
$check['status'] ?? null,
|
||
);
|
||
|
||
$severitySpec = \App\Support\Badges\BadgeRenderer::spec(
|
||
\App\Support\Badges\BadgeDomain::VerificationCheckSeverity,
|
||
$check['severity'] ?? null,
|
||
);
|
||
|
||
$ack = $checkKey !== '' ? ($ackByKey[$checkKey] ?? null) : null;
|
||
$ack = is_array($ack) ? $ack : null;
|
||
|
||
$ackReason = $ack['ack_reason'] ?? null;
|
||
$ackReason = is_string($ackReason) && trim($ackReason) !== '' ? trim($ackReason) : null;
|
||
|
||
$ackAt = $ack['acknowledged_at'] ?? null;
|
||
$ackAt = is_string($ackAt) && trim($ackAt) !== '' ? trim($ackAt) : null;
|
||
|
||
$ackBy = $ack['acknowledged_by'] ?? null;
|
||
$ackBy = is_array($ackBy) ? $ackBy : null;
|
||
|
||
$ackByName = $ackBy['name'] ?? null;
|
||
$ackByName = is_string($ackByName) && trim($ackByName) !== '' ? trim($ackByName) : null;
|
||
|
||
$expiresAt = $ack['expires_at'] ?? null;
|
||
$expiresAt = is_string($expiresAt) && trim($expiresAt) !== '' ? trim($expiresAt) : null;
|
||
@endphp
|
||
|
||
<div class="rounded-lg border border-gray-100 bg-gray-50 p-4 dark:border-gray-800 dark:bg-gray-950">
|
||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||
<div class="space-y-1">
|
||
<div class="text-sm font-medium text-gray-900 dark:text-white">
|
||
{{ $title }}
|
||
</div>
|
||
@if ($message)
|
||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||
{{ $message }}
|
||
</div>
|
||
@endif
|
||
</div>
|
||
|
||
<div class="flex shrink-0 flex-wrap items-center justify-end gap-2">
|
||
<x-filament::badge :color="$severitySpec->color" :icon="$severitySpec->icon" size="sm">
|
||
{{ $severitySpec->label }}
|
||
</x-filament::badge>
|
||
<x-filament::badge :color="$statusSpec->color" :icon="$statusSpec->icon" size="sm">
|
||
{{ $statusSpec->label }}
|
||
</x-filament::badge>
|
||
</div>
|
||
</div>
|
||
|
||
@if ($ackReason || $ackAt || $ackByName || $expiresAt)
|
||
<div class="mt-3 space-y-1 text-sm text-gray-700 dark:text-gray-200">
|
||
@if ($ackReason)
|
||
<div>
|
||
<span class="font-semibold">Reason:</span> {{ $ackReason }}
|
||
</div>
|
||
@endif
|
||
@if ($ackByName || $ackAt)
|
||
<div>
|
||
<span class="font-semibold">Acknowledged:</span>
|
||
@if ($ackByName)
|
||
{{ $ackByName }}
|
||
@endif
|
||
@if ($ackAt)
|
||
<span class="text-gray-500 dark:text-gray-400">({{ $ackAt }})</span>
|
||
@endif
|
||
</div>
|
||
@endif
|
||
@if ($expiresAt)
|
||
<div>
|
||
<span class="font-semibold">Expires:</span>
|
||
<span class="text-gray-500 dark:text-gray-400">{{ $expiresAt }}</span>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
@endif
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
</details>
|
||
@endif
|
||
</div>
|
||
@endif
|
||
</div>
|
||
|
||
<div x-show="tab === 'passed'" style="display: none;">
|
||
@if ($passed === [])
|
||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||
No passing checks recorded.
|
||
</div>
|
||
@else
|
||
<div class="space-y-2">
|
||
@foreach ($passed as $check)
|
||
@php
|
||
$check = is_array($check) ? $check : [];
|
||
|
||
$title = $check['title'] ?? 'Check';
|
||
$title = is_string($title) && trim($title) !== '' ? trim($title) : 'Check';
|
||
|
||
$statusSpec = \App\Support\Badges\BadgeRenderer::spec(
|
||
\App\Support\Badges\BadgeDomain::VerificationCheckStatus,
|
||
$check['status'] ?? null,
|
||
);
|
||
@endphp
|
||
|
||
<div class="flex items-center justify-between gap-3 rounded-lg border border-gray-200 bg-white p-3 text-sm shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||
<div class="font-medium text-gray-900 dark:text-white">
|
||
{{ $title }}
|
||
</div>
|
||
<x-filament::badge :color="$statusSpec->color" :icon="$statusSpec->icon" size="sm">
|
||
{{ $statusSpec->label }}
|
||
</x-filament::badge>
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
@endif
|
||
</div>
|
||
|
||
<div x-show="tab === 'technical'" style="display: none;">
|
||
<div class="space-y-4 text-sm text-gray-700 dark:text-gray-200">
|
||
<div class="space-y-1">
|
||
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||
Identifiers
|
||
</div>
|
||
<div class="flex flex-col gap-1">
|
||
<div>
|
||
<span class="text-gray-500 dark:text-gray-400">Run ID:</span>
|
||
<span class="font-mono">{{ (int) ($run['id'] ?? 0) }}</span>
|
||
</div>
|
||
<div>
|
||
<span class="text-gray-500 dark:text-gray-400">Flow:</span>
|
||
<span class="font-mono">{{ (string) ($run['type'] ?? '') }}</span>
|
||
</div>
|
||
@if ($fingerprint)
|
||
<div>
|
||
<span class="text-gray-500 dark:text-gray-400">Fingerprint:</span>
|
||
<span class="font-mono text-xs break-all">{{ $fingerprint }}</span>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
|
||
@if ($previousRunUrl !== null)
|
||
<div>
|
||
<a
|
||
href="{{ $previousRunUrl }}"
|
||
class="font-medium text-primary-600 hover:underline dark:text-primary-400"
|
||
>
|
||
Open previous verification
|
||
</a>
|
||
</div>
|
||
@endif
|
||
|
||
@if ($runUrl !== null)
|
||
<div>
|
||
<a
|
||
href="{{ $runUrl }}"
|
||
class="font-medium text-primary-600 hover:underline dark:text-primary-400"
|
||
>
|
||
Open run details
|
||
</a>
|
||
</div>
|
||
@endif
|
||
|
||
@if ($targetScope !== [])
|
||
<div class="space-y-1">
|
||
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||
Target scope
|
||
</div>
|
||
<div class="flex flex-col gap-1">
|
||
@php
|
||
$entraTenantId = $targetScope['entra_tenant_id'] ?? null;
|
||
$entraTenantName = $targetScope['entra_tenant_name'] ?? null;
|
||
|
||
$entraTenantId = is_string($entraTenantId) && $entraTenantId !== '' ? $entraTenantId : null;
|
||
$entraTenantName = is_string($entraTenantName) && $entraTenantName !== '' ? $entraTenantName : null;
|
||
@endphp
|
||
|
||
@if ($entraTenantName !== null)
|
||
<div>
|
||
<span class="text-gray-500 dark:text-gray-400">Entra tenant:</span>
|
||
<span class="font-medium text-gray-900 dark:text-gray-100">{{ $entraTenantName }}</span>
|
||
</div>
|
||
@endif
|
||
|
||
@if ($entraTenantId !== null)
|
||
<div>
|
||
<span class="text-gray-500 dark:text-gray-400">Entra tenant ID:</span>
|
||
<span class="font-mono text-xs break-all text-gray-900 dark:text-gray-100">{{ $entraTenantId }}</span>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
@endif
|
||
</x-filament::section>
|
||
</div>
|
||
</x-dynamic-component>
|