feat: harden canonical required permissions surface and issues-first UX
This commit is contained in:
parent
96760c65e6
commit
43dff0f2f4
@ -5,7 +5,6 @@
|
|||||||
namespace App\Filament\Pages;
|
namespace App\Filament\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\ProviderConnectionResource;
|
use App\Filament\Resources\ProviderConnectionResource;
|
||||||
use App\Models\ProviderConnection;
|
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\WorkspaceMembership;
|
use App\Models\WorkspaceMembership;
|
||||||
@ -41,34 +40,28 @@ class TenantRequiredPermissions extends Page
|
|||||||
*/
|
*/
|
||||||
public array $viewModel = [];
|
public array $viewModel = [];
|
||||||
|
|
||||||
|
public ?Tenant $scopedTenant = null;
|
||||||
|
|
||||||
public static function canAccess(): bool
|
public static function canAccess(): bool
|
||||||
{
|
{
|
||||||
$tenant = static::resolveScopedTenant();
|
return static::hasScopedTenantAccess(static::resolveScopedTenant());
|
||||||
$user = auth()->user();
|
|
||||||
|
|
||||||
if (! $tenant instanceof Tenant || ! $user instanceof User) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());
|
|
||||||
|
|
||||||
if ($workspaceId === null || (int) $tenant->workspace_id !== (int) $workspaceId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return WorkspaceMembership::query()
|
|
||||||
->where('workspace_id', (int) $workspaceId)
|
|
||||||
->where('user_id', (int) $user->getKey())
|
|
||||||
->exists();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function currentTenant(): ?Tenant
|
public function currentTenant(): ?Tenant
|
||||||
{
|
{
|
||||||
return static::resolveScopedTenant();
|
return $this->scopedTenant;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function mount(): void
|
public function mount(): void
|
||||||
{
|
{
|
||||||
|
$tenant = static::resolveScopedTenant();
|
||||||
|
|
||||||
|
if (! $tenant instanceof Tenant || ! static::hasScopedTenantAccess($tenant)) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->scopedTenant = $tenant;
|
||||||
|
|
||||||
$queryFeatures = request()->query('features', $this->features);
|
$queryFeatures = request()->query('features', $this->features);
|
||||||
|
|
||||||
$state = TenantRequiredPermissionsViewModelBuilder::normalizeFilterState([
|
$state = TenantRequiredPermissionsViewModelBuilder::normalizeFilterState([
|
||||||
@ -147,7 +140,7 @@ public function resetFilters(): void
|
|||||||
|
|
||||||
private function refreshViewModel(): void
|
private function refreshViewModel(): void
|
||||||
{
|
{
|
||||||
$tenant = static::resolveScopedTenant();
|
$tenant = $this->scopedTenant;
|
||||||
|
|
||||||
if (! $tenant instanceof Tenant) {
|
if (! $tenant instanceof Tenant) {
|
||||||
$this->viewModel = [];
|
$this->viewModel = [];
|
||||||
@ -174,25 +167,20 @@ private function refreshViewModel(): void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function reRunVerificationUrl(): ?string
|
public function reRunVerificationUrl(): string
|
||||||
{
|
{
|
||||||
$tenant = static::resolveScopedTenant();
|
return route('admin.onboarding');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function manageProviderConnectionUrl(): ?string
|
||||||
|
{
|
||||||
|
$tenant = $this->scopedTenant;
|
||||||
|
|
||||||
if (! $tenant instanceof Tenant) {
|
if (! $tenant instanceof Tenant) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$connectionId = ProviderConnection::query()
|
return ProviderConnectionResource::getUrl('index', ['tenant' => $tenant], panel: 'admin');
|
||||||
->where('tenant_id', (int) $tenant->getKey())
|
|
||||||
->orderByDesc('is_default')
|
|
||||||
->orderByDesc('id')
|
|
||||||
->value('id');
|
|
||||||
|
|
||||||
if (! is_int($connectionId)) {
|
|
||||||
return ProviderConnectionResource::getUrl('index', ['tenant' => $tenant], panel: 'admin');
|
|
||||||
}
|
|
||||||
|
|
||||||
return ProviderConnectionResource::getUrl('edit', ['tenant' => $tenant, 'record' => $connectionId], panel: 'admin');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static function resolveScopedTenant(): ?Tenant
|
protected static function resolveScopedTenant(): ?Tenant
|
||||||
@ -209,6 +197,32 @@ protected static function resolveScopedTenant(): ?Tenant
|
|||||||
->first();
|
->first();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Tenant::current();
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function hasScopedTenantAccess(?Tenant $tenant): bool
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if (! $tenant instanceof Tenant || ! $user instanceof User) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());
|
||||||
|
|
||||||
|
if ($workspaceId === null || (int) $tenant->workspace_id !== (int) $workspaceId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$isWorkspaceMember = WorkspaceMembership::query()
|
||||||
|
->where('workspace_id', (int) $workspaceId)
|
||||||
|
->where('user_id', (int) $user->getKey())
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if (! $isWorkspaceMember) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $user->canAccessTenant($tenant);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,8 @@
|
|||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\TenantPermission;
|
use App\Models\TenantPermission;
|
||||||
use App\Services\Graph\GraphClientInterface;
|
use App\Services\Graph\GraphClientInterface;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
class TenantPermissionService
|
class TenantPermissionService
|
||||||
{
|
{
|
||||||
@ -44,6 +46,7 @@ public function getGrantedPermissions(Tenant $tenant): array
|
|||||||
* @return array{
|
* @return array{
|
||||||
* overall_status:string,
|
* overall_status:string,
|
||||||
* permissions:array<int,array{key:string,type:string,description:?string,features:array<int,string>,status:string,details:array<string,mixed>|null}>,
|
* permissions:array<int,array{key:string,type:string,description:?string,features:array<int,string>,status:string,details:array<string,mixed>|null}>,
|
||||||
|
* last_refreshed_at:?string,
|
||||||
* live_check?: array{attempted:bool,succeeded:bool,http_status:?int,reason_code:?string}
|
* live_check?: array{attempted:bool,succeeded:bool,http_status:?int,reason_code:?string}
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
@ -210,6 +213,7 @@ public function compare(
|
|||||||
$payload = [
|
$payload = [
|
||||||
'overall_status' => $overall,
|
'overall_status' => $overall,
|
||||||
'permissions' => $results,
|
'permissions' => $results,
|
||||||
|
'last_refreshed_at' => $this->lastRefreshedAtIso($tenant),
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($liveCheckMeta['attempted'] === true) {
|
if ($liveCheckMeta['attempted'] === true) {
|
||||||
@ -389,4 +393,25 @@ private function fetchLivePermissions(Tenant $tenant, ?array $graphOptions = nul
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function lastRefreshedAtIso(Tenant $tenant): ?string
|
||||||
|
{
|
||||||
|
$lastCheckedAt = TenantPermission::query()
|
||||||
|
->where('tenant_id', (int) $tenant->getKey())
|
||||||
|
->max('last_checked_at');
|
||||||
|
|
||||||
|
if ($lastCheckedAt instanceof DateTimeInterface) {
|
||||||
|
return Carbon::instance($lastCheckedAt)->toIso8601String();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($lastCheckedAt) && $lastCheckedAt !== '') {
|
||||||
|
try {
|
||||||
|
return Carbon::parse($lastCheckedAt)->toIso8601String();
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Support\Verification\VerificationReportOverall;
|
use App\Support\Verification\VerificationReportOverall;
|
||||||
|
use Carbon\CarbonInterface;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
class TenantRequiredPermissionsViewModelBuilder
|
class TenantRequiredPermissionsViewModelBuilder
|
||||||
{
|
{
|
||||||
@ -16,7 +18,8 @@ class TenantRequiredPermissionsViewModelBuilder
|
|||||||
* overview: array{
|
* overview: array{
|
||||||
* overall: string,
|
* overall: string,
|
||||||
* counts: array{missing_application:int,missing_delegated:int,present:int,error:int},
|
* counts: array{missing_application:int,missing_delegated:int,present:int,error:int},
|
||||||
* feature_impacts: array<int, FeatureImpact>
|
* feature_impacts: array<int, FeatureImpact>,
|
||||||
|
* freshness: array{last_refreshed_at:?string,is_stale:bool}
|
||||||
* },
|
* },
|
||||||
* permissions: array<int, TenantPermissionRow>,
|
* permissions: array<int, TenantPermissionRow>,
|
||||||
* filters: FilterState,
|
* filters: FilterState,
|
||||||
@ -48,6 +51,7 @@ public function build(Tenant $tenant, array $filters = []): array
|
|||||||
$state = self::normalizeFilterState($filters);
|
$state = self::normalizeFilterState($filters);
|
||||||
|
|
||||||
$filteredPermissions = self::applyFilterState($allPermissions, $state);
|
$filteredPermissions = self::applyFilterState($allPermissions, $state);
|
||||||
|
$freshness = self::deriveFreshness(self::parseLastRefreshedAt($comparison['last_refreshed_at'] ?? null));
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'tenant' => [
|
'tenant' => [
|
||||||
@ -56,9 +60,10 @@ public function build(Tenant $tenant, array $filters = []): array
|
|||||||
'name' => (string) $tenant->name,
|
'name' => (string) $tenant->name,
|
||||||
],
|
],
|
||||||
'overview' => [
|
'overview' => [
|
||||||
'overall' => self::deriveOverallStatus($allPermissions),
|
'overall' => self::deriveOverallStatus($allPermissions, (bool) ($freshness['is_stale'] ?? true)),
|
||||||
'counts' => self::deriveCounts($allPermissions),
|
'counts' => self::deriveCounts($allPermissions),
|
||||||
'feature_impacts' => self::deriveFeatureImpacts($allPermissions),
|
'feature_impacts' => self::deriveFeatureImpacts($allPermissions),
|
||||||
|
'freshness' => $freshness,
|
||||||
],
|
],
|
||||||
'permissions' => $filteredPermissions,
|
'permissions' => $filteredPermissions,
|
||||||
'filters' => $state,
|
'filters' => $state,
|
||||||
@ -72,7 +77,7 @@ public function build(Tenant $tenant, array $filters = []): array
|
|||||||
/**
|
/**
|
||||||
* @param array<int, TenantPermissionRow> $permissions
|
* @param array<int, TenantPermissionRow> $permissions
|
||||||
*/
|
*/
|
||||||
public static function deriveOverallStatus(array $permissions): string
|
public static function deriveOverallStatus(array $permissions, bool $hasStaleFreshness = false): string
|
||||||
{
|
{
|
||||||
$hasMissingApplication = collect($permissions)->contains(
|
$hasMissingApplication = collect($permissions)->contains(
|
||||||
fn (array $row): bool => $row['status'] === 'missing' && $row['type'] === 'application',
|
fn (array $row): bool => $row['status'] === 'missing' && $row['type'] === 'application',
|
||||||
@ -90,13 +95,35 @@ public static function deriveOverallStatus(array $permissions): string
|
|||||||
fn (array $row): bool => $row['status'] === 'missing' && $row['type'] === 'delegated',
|
fn (array $row): bool => $row['status'] === 'missing' && $row['type'] === 'delegated',
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($hasErrors || $hasMissingDelegated) {
|
if ($hasErrors || $hasMissingDelegated || $hasStaleFreshness) {
|
||||||
return VerificationReportOverall::NeedsAttention->value;
|
return VerificationReportOverall::NeedsAttention->value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return VerificationReportOverall::Ready->value;
|
return VerificationReportOverall::Ready->value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{last_refreshed_at:?string,is_stale:bool}
|
||||||
|
*/
|
||||||
|
public static function deriveFreshness(?CarbonInterface $lastRefreshedAt, ?CarbonInterface $referenceTime = null): array
|
||||||
|
{
|
||||||
|
$reference = $referenceTime instanceof Carbon
|
||||||
|
? $referenceTime->copy()
|
||||||
|
: ($referenceTime !== null ? Carbon::instance($referenceTime) : now());
|
||||||
|
|
||||||
|
$lastRefreshed = $lastRefreshedAt instanceof Carbon
|
||||||
|
? $lastRefreshedAt
|
||||||
|
: ($lastRefreshedAt !== null ? Carbon::instance($lastRefreshedAt) : null);
|
||||||
|
|
||||||
|
$isStale = $lastRefreshed === null
|
||||||
|
|| $lastRefreshed->lt($reference->copy()->subDays(30));
|
||||||
|
|
||||||
|
return [
|
||||||
|
'last_refreshed_at' => $lastRefreshed?->toIso8601String(),
|
||||||
|
'is_stale' => $isStale,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<int, TenantPermissionRow> $permissions
|
* @param array<int, TenantPermissionRow> $permissions
|
||||||
* @return array{missing_application:int,missing_delegated:int,present:int,error:int}
|
* @return array{missing_application:int,missing_delegated:int,present:int,error:int}
|
||||||
@ -386,4 +413,25 @@ private static function normalizePermissionRow(array $row): array
|
|||||||
'details' => $details,
|
'details' => $details,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static function parseLastRefreshedAt(mixed $value): ?Carbon
|
||||||
|
{
|
||||||
|
if ($value instanceof Carbon) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof CarbonInterface) {
|
||||||
|
return Carbon::instance($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($value) && $value !== '') {
|
||||||
|
try {
|
||||||
|
return Carbon::parse($value);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
use App\Support\Badges\BadgeDomain;
|
use App\Support\Badges\BadgeDomain;
|
||||||
use App\Support\Badges\BadgeRenderer;
|
use App\Support\Badges\BadgeRenderer;
|
||||||
use App\Support\Links\RequiredPermissionsLinks;
|
use App\Support\Links\RequiredPermissionsLinks;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
$tenant = $this->currentTenant();
|
$tenant = $this->currentTenant();
|
||||||
|
|
||||||
@ -9,6 +10,7 @@
|
|||||||
$overview = is_array($vm['overview'] ?? null) ? $vm['overview'] : [];
|
$overview = is_array($vm['overview'] ?? null) ? $vm['overview'] : [];
|
||||||
$counts = is_array($overview['counts'] ?? null) ? $overview['counts'] : [];
|
$counts = is_array($overview['counts'] ?? null) ? $overview['counts'] : [];
|
||||||
$featureImpacts = is_array($overview['feature_impacts'] ?? null) ? $overview['feature_impacts'] : [];
|
$featureImpacts = is_array($overview['feature_impacts'] ?? null) ? $overview['feature_impacts'] : [];
|
||||||
|
$freshness = is_array($overview['freshness'] ?? null) ? $overview['freshness'] : [];
|
||||||
|
|
||||||
$filters = is_array($vm['filters'] ?? null) ? $vm['filters'] : [];
|
$filters = is_array($vm['filters'] ?? null) ? $vm['filters'] : [];
|
||||||
$selectedFeatures = is_array($filters['features'] ?? null) ? $filters['features'] : [];
|
$selectedFeatures = is_array($filters['features'] ?? null) ? $filters['features'] : [];
|
||||||
@ -47,17 +49,77 @@
|
|||||||
$adminConsentLabel = $adminConsentUrl ? 'Open admin consent' : 'Admin consent guide';
|
$adminConsentLabel = $adminConsentUrl ? 'Open admin consent' : 'Admin consent guide';
|
||||||
|
|
||||||
$reRunUrl = $this->reRunVerificationUrl();
|
$reRunUrl = $this->reRunVerificationUrl();
|
||||||
|
$manageProviderConnectionUrl = $this->manageProviderConnectionUrl();
|
||||||
|
$lastRefreshedAt = is_string($freshness['last_refreshed_at'] ?? null) ? (string) $freshness['last_refreshed_at'] : null;
|
||||||
|
$lastRefreshedLabel = $lastRefreshedAt ? Carbon::parse($lastRefreshedAt)->diffForHumans() : 'Unknown';
|
||||||
|
$isStale = (bool) ($freshness['is_stale'] ?? true);
|
||||||
|
$hasStoredPermissionData = $lastRefreshedAt !== null;
|
||||||
|
|
||||||
|
$issues = [];
|
||||||
|
|
||||||
|
if ($missingApplication > 0) {
|
||||||
|
$issues[] = [
|
||||||
|
'severity' => 'Blocker',
|
||||||
|
'title' => 'Missing application permissions',
|
||||||
|
'description' => "{$missingApplication} required application permission(s) are missing.",
|
||||||
|
'links' => array_values(array_filter([
|
||||||
|
['label' => $adminConsentLabel, 'url' => $adminConsentPrimaryUrl, 'external' => true],
|
||||||
|
$manageProviderConnectionUrl ? ['label' => 'Manage provider connection', 'url' => $manageProviderConnectionUrl, 'external' => false] : null,
|
||||||
|
['label' => 'Re-run verification', 'url' => $reRunUrl, 'external' => false],
|
||||||
|
])),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($missingDelegated > 0) {
|
||||||
|
$issues[] = [
|
||||||
|
'severity' => 'Warning',
|
||||||
|
'title' => 'Missing delegated permissions',
|
||||||
|
'description' => "{$missingDelegated} delegated permission(s) are missing.",
|
||||||
|
'links' => [
|
||||||
|
['label' => $adminConsentLabel, 'url' => $adminConsentPrimaryUrl, 'external' => true],
|
||||||
|
['label' => 'Re-run verification', 'url' => $reRunUrl, 'external' => false],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($errorCount > 0) {
|
||||||
|
$issues[] = [
|
||||||
|
'severity' => 'Warning',
|
||||||
|
'title' => 'Verification results need review',
|
||||||
|
'description' => "{$errorCount} permission row(s) are in an unknown/error state and require follow-up.",
|
||||||
|
'links' => [
|
||||||
|
['label' => 'Re-run verification', 'url' => $reRunUrl, 'external' => false],
|
||||||
|
$manageProviderConnectionUrl ? ['label' => 'Manage provider connection', 'url' => $manageProviderConnectionUrl, 'external' => false] : ['label' => 'Admin consent guide', 'url' => RequiredPermissionsLinks::adminConsentGuideUrl(), 'external' => true],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isStale) {
|
||||||
|
$issues[] = [
|
||||||
|
'severity' => 'Warning',
|
||||||
|
'title' => 'Freshness warning',
|
||||||
|
'description' => $hasStoredPermissionData
|
||||||
|
? "Permission data is older than 30 days (last refresh {$lastRefreshedLabel})."
|
||||||
|
: 'No stored verification data is available yet.',
|
||||||
|
'links' => [
|
||||||
|
['label' => 'Start verification', 'url' => $reRunUrl, 'external' => false],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<x-filament::page>
|
<x-filament::page>
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<x-filament::section>
|
<x-filament::section heading="Summary">
|
||||||
<div class="flex flex-col gap-4" x-data="{ showCopyApplication: false, showCopyDelegated: false }">
|
<div class="flex flex-col gap-4" x-data="{ showCopyApplication: false, showCopyDelegated: false }">
|
||||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
Review what’s missing for this tenant and copy the missing permissions for admin consent.
|
Review what’s missing for this tenant and copy the missing permissions for admin consent.
|
||||||
</div>
|
</div>
|
||||||
|
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Stored-data view only. Last refreshed: {{ $lastRefreshedLabel }}{{ $isStale ? ' (stale)' : '' }}.
|
||||||
|
</div>
|
||||||
|
|
||||||
@if ($overallSpec)
|
@if ($overallSpec)
|
||||||
<x-filament::badge :color="$overallSpec->color" :icon="$overallSpec->icon">
|
<x-filament::badge :color="$overallSpec->color" :icon="$overallSpec->icon">
|
||||||
@ -86,6 +148,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (! $hasStoredPermissionData)
|
||||||
|
<div class="rounded-xl border border-warning-200 bg-warning-50 p-4 text-sm text-warning-800 dark:border-warning-800 dark:bg-warning-950/30 dark:text-warning-200">
|
||||||
|
<div class="font-semibold">Keine Daten verfügbar</div>
|
||||||
|
<div class="mt-1">
|
||||||
|
Für diesen Tenant liegen noch keine gespeicherten Verifikationsdaten vor.
|
||||||
|
<a href="{{ $reRunUrl }}" class="font-medium underline">Start verification</a>.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<div class="rounded-xl border border-gray-200 bg-white p-4 text-sm text-gray-700 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-200">
|
<div class="rounded-xl border border-gray-200 bg-white p-4 text-sm text-gray-700 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-200">
|
||||||
<div class="text-sm font-semibold text-gray-900 dark:text-white">Guidance</div>
|
<div class="text-sm font-semibold text-gray-900 dark:text-white">Guidance</div>
|
||||||
<div class="mt-2 space-y-1">
|
<div class="mt-2 space-y-1">
|
||||||
@ -322,7 +394,75 @@ class="mt-4 space-y-2"
|
|||||||
</div>
|
</div>
|
||||||
</x-filament::section>
|
</x-filament::section>
|
||||||
|
|
||||||
<x-filament::section heading="Details">
|
<x-filament::section heading="Issues">
|
||||||
|
@if ($issues === [])
|
||||||
|
<div class="rounded-xl border border-success-200 bg-success-50 p-4 text-sm text-success-800 dark:border-success-800 dark:bg-success-950/30 dark:text-success-200">
|
||||||
|
No blockers or warnings detected from stored data.
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="space-y-3">
|
||||||
|
@foreach ($issues as $issue)
|
||||||
|
@php
|
||||||
|
$severity = (string) ($issue['severity'] ?? 'Warning');
|
||||||
|
$severityColor = $severity === 'Blocker' ? 'danger' : 'warning';
|
||||||
|
$title = (string) ($issue['title'] ?? 'Issue');
|
||||||
|
$description = (string) ($issue['description'] ?? '');
|
||||||
|
$links = is_array($issue['links'] ?? null) ? $issue['links'] : [];
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div class="rounded-xl border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-gray-900">
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<x-filament::badge :color="$severityColor" size="sm">{{ $severity }}</x-filament::badge>
|
||||||
|
<div class="text-sm font-semibold text-gray-950 dark:text-white">{{ $title }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-sm text-gray-700 dark:text-gray-300">{{ $description }}</div>
|
||||||
|
@if ($links !== [])
|
||||||
|
<div class="mt-3 flex flex-wrap gap-3 text-sm">
|
||||||
|
@foreach ($links as $link)
|
||||||
|
@php
|
||||||
|
$label = is_array($link) ? (string) ($link['label'] ?? '') : '';
|
||||||
|
$url = is_array($link) ? (string) ($link['url'] ?? '') : '';
|
||||||
|
$external = is_array($link) ? (bool) ($link['external'] ?? false) : false;
|
||||||
|
@endphp
|
||||||
|
@if ($label !== '' && $url !== '')
|
||||||
|
<a
|
||||||
|
href="{{ $url }}"
|
||||||
|
class="text-primary-600 hover:underline dark:text-primary-400"
|
||||||
|
@if ($external)
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
@endif
|
||||||
|
>
|
||||||
|
{{ $label }}
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</x-filament::section>
|
||||||
|
|
||||||
|
<x-filament::section heading="Passed">
|
||||||
|
<div class="rounded-xl border border-gray-200 bg-white p-4 text-sm text-gray-700 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-200">
|
||||||
|
<div class="font-semibold text-gray-950 dark:text-white">
|
||||||
|
{{ $presentCount }} permission(s) currently pass.
|
||||||
|
</div>
|
||||||
|
<div class="mt-1">
|
||||||
|
{{ $requiredTotal > 0 ? "Out of {$requiredTotal} required permissions, {$presentCount} are currently granted." : 'No required permissions are configured yet.' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-filament::section>
|
||||||
|
|
||||||
|
<x-filament::section heading="Technical details">
|
||||||
|
<details data-testid="technical-details" class="group rounded-xl border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-gray-900">
|
||||||
|
<summary class="cursor-pointer list-none text-sm font-semibold text-gray-900 dark:text-white">
|
||||||
|
Expand technical details
|
||||||
|
</summary>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
@if (! $tenant)
|
@if (! $tenant)
|
||||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
No tenant selected.
|
No tenant selected.
|
||||||
@ -507,6 +647,8 @@ class="align-top"
|
|||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</x-filament::section>
|
</x-filament::section>
|
||||||
</div>
|
</div>
|
||||||
</x-filament::page>
|
</x-filament::page>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user