058-tenant-ui-polish #70
@ -4,35 +4,46 @@
|
|||||||
|
|
||||||
namespace App\Filament\Widgets\Dashboard;
|
namespace App\Filament\Widgets\Dashboard;
|
||||||
|
|
||||||
|
use App\Filament\Resources\FindingResource;
|
||||||
|
use App\Filament\Resources\OperationRunResource;
|
||||||
use App\Models\Finding;
|
use App\Models\Finding;
|
||||||
use App\Models\OperationRun;
|
use App\Models\OperationRun;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Support\OpsUx\ActiveRuns;
|
use App\Support\OpsUx\ActiveRuns;
|
||||||
use Filament\Facades\Filament;
|
use Filament\Facades\Filament;
|
||||||
use Filament\Widgets\Widget;
|
use Filament\Widgets\StatsOverviewWidget;
|
||||||
|
use Filament\Widgets\StatsOverviewWidget\Stat;
|
||||||
|
|
||||||
class DashboardKpis extends Widget
|
class DashboardKpis extends StatsOverviewWidget
|
||||||
{
|
{
|
||||||
protected static bool $isLazy = false;
|
protected static bool $isLazy = false;
|
||||||
|
|
||||||
protected string $view = 'filament.widgets.dashboard.dashboard-kpis';
|
|
||||||
|
|
||||||
protected int|string|array $columnSpan = 'full';
|
protected int|string|array $columnSpan = 'full';
|
||||||
|
|
||||||
|
protected function getPollingInterval(): ?string
|
||||||
|
{
|
||||||
|
$tenant = Filament::getTenant();
|
||||||
|
|
||||||
|
if (! $tenant instanceof Tenant) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ActiveRuns::existForTenant($tenant) ? '10s' : null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string, mixed>
|
* @return array<Stat>
|
||||||
*/
|
*/
|
||||||
protected function getViewData(): array
|
protected function getStats(): array
|
||||||
{
|
{
|
||||||
$tenant = Filament::getTenant();
|
$tenant = Filament::getTenant();
|
||||||
|
|
||||||
if (! $tenant instanceof Tenant) {
|
if (! $tenant instanceof Tenant) {
|
||||||
return [
|
return [
|
||||||
'pollingInterval' => null,
|
Stat::make('Open drift findings', 0),
|
||||||
'openDriftFindings' => 0,
|
Stat::make('High severity drift', 0),
|
||||||
'highSeverityDriftFindings' => 0,
|
Stat::make('Active operations', 0),
|
||||||
'activeRuns' => 0,
|
Stat::make('Inventory active', 0),
|
||||||
'inventoryActiveRuns' => 0,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,11 +74,17 @@ protected function getViewData(): array
|
|||||||
->count();
|
->count();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'pollingInterval' => ActiveRuns::existForTenant($tenant) ? '10s' : null,
|
Stat::make('Open drift findings', $openDriftFindings)
|
||||||
'openDriftFindings' => $openDriftFindings,
|
->url(FindingResource::getUrl('index', tenant: $tenant)),
|
||||||
'highSeverityDriftFindings' => $highSeverityDriftFindings,
|
Stat::make('High severity drift', $highSeverityDriftFindings)
|
||||||
'activeRuns' => $activeRuns,
|
->color($highSeverityDriftFindings > 0 ? 'danger' : 'gray')
|
||||||
'inventoryActiveRuns' => $inventoryActiveRuns,
|
->url(FindingResource::getUrl('index', tenant: $tenant)),
|
||||||
|
Stat::make('Active operations', $activeRuns)
|
||||||
|
->color($activeRuns > 0 ? 'warning' : 'gray')
|
||||||
|
->url(OperationRunResource::getUrl('index', tenant: $tenant)),
|
||||||
|
Stat::make('Inventory active', $inventoryActiveRuns)
|
||||||
|
->color($inventoryActiveRuns > 0 ? 'warning' : 'gray')
|
||||||
|
->url(OperationRunResource::getUrl('index', tenant: $tenant)),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,6 +31,7 @@ protected function getViewData(): array
|
|||||||
return [
|
return [
|
||||||
'pollingInterval' => null,
|
'pollingInterval' => null,
|
||||||
'items' => [],
|
'items' => [],
|
||||||
|
'healthyChecks' => [],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,6 +51,8 @@ protected function getViewData(): array
|
|||||||
'title' => 'High severity drift findings',
|
'title' => 'High severity drift findings',
|
||||||
'body' => "{$highSeverityCount} finding(s) need review.",
|
'body' => "{$highSeverityCount} finding(s) need review.",
|
||||||
'url' => FindingResource::getUrl('index', tenant: $tenant),
|
'url' => FindingResource::getUrl('index', tenant: $tenant),
|
||||||
|
'badge' => 'Drift',
|
||||||
|
'badgeColor' => 'danger',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,6 +70,8 @@ protected function getViewData(): array
|
|||||||
'title' => 'No drift scan yet',
|
'title' => 'No drift scan yet',
|
||||||
'body' => 'Generate drift after you have at least two successful inventory runs.',
|
'body' => 'Generate drift after you have at least two successful inventory runs.',
|
||||||
'url' => DriftLanding::getUrl(tenant: $tenant),
|
'url' => DriftLanding::getUrl(tenant: $tenant),
|
||||||
|
'badge' => 'Drift',
|
||||||
|
'badgeColor' => 'warning',
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
$isStale = $latestDriftSuccess->completed_at?->lt(now()->subDays(7)) ?? true;
|
$isStale = $latestDriftSuccess->completed_at?->lt(now()->subDays(7)) ?? true;
|
||||||
@ -76,6 +81,8 @@ protected function getViewData(): array
|
|||||||
'title' => 'Drift stale',
|
'title' => 'Drift stale',
|
||||||
'body' => 'Last drift scan is older than 7 days.',
|
'body' => 'Last drift scan is older than 7 days.',
|
||||||
'url' => DriftLanding::getUrl(tenant: $tenant),
|
'url' => DriftLanding::getUrl(tenant: $tenant),
|
||||||
|
'badge' => 'Drift',
|
||||||
|
'badgeColor' => 'warning',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -93,6 +100,8 @@ protected function getViewData(): array
|
|||||||
'title' => 'Drift generation failed',
|
'title' => 'Drift generation failed',
|
||||||
'body' => 'Investigate the latest failed run.',
|
'body' => 'Investigate the latest failed run.',
|
||||||
'url' => OperationRunLinks::view($latestDriftFailure, $tenant),
|
'url' => OperationRunLinks::view($latestDriftFailure, $tenant),
|
||||||
|
'badge' => 'Operations',
|
||||||
|
'badgeColor' => 'danger',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,12 +115,44 @@ protected function getViewData(): array
|
|||||||
'title' => 'Operations in progress',
|
'title' => 'Operations in progress',
|
||||||
'body' => "{$activeRuns} run(s) are active.",
|
'body' => "{$activeRuns} run(s) are active.",
|
||||||
'url' => OperationRunLinks::index($tenant),
|
'url' => OperationRunLinks::index($tenant),
|
||||||
|
'badge' => 'Operations',
|
||||||
|
'badgeColor' => 'warning',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = array_slice($items, 0, 5);
|
||||||
|
|
||||||
|
$healthyChecks = [];
|
||||||
|
|
||||||
|
if ($items === []) {
|
||||||
|
$healthyChecks = [
|
||||||
|
[
|
||||||
|
'title' => 'Drift findings look healthy',
|
||||||
|
'body' => 'No high severity drift findings are open.',
|
||||||
|
'url' => FindingResource::getUrl('index', tenant: $tenant),
|
||||||
|
'linkLabel' => 'View findings',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'Drift scans are up to date',
|
||||||
|
'body' => $latestDriftSuccess?->completed_at
|
||||||
|
? 'Last drift scan: '.$latestDriftSuccess->completed_at->diffForHumans(['short' => true]).'.'
|
||||||
|
: 'Drift scan history is available in Drift.',
|
||||||
|
'url' => DriftLanding::getUrl(tenant: $tenant),
|
||||||
|
'linkLabel' => 'Open Drift',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'No active operations',
|
||||||
|
'body' => 'Nothing is currently running for this tenant.',
|
||||||
|
'url' => OperationRunLinks::index($tenant),
|
||||||
|
'linkLabel' => 'View operations',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'pollingInterval' => ActiveRuns::existForTenant($tenant) ? '10s' : null,
|
'pollingInterval' => ActiveRuns::existForTenant($tenant) ? '10s' : null,
|
||||||
'items' => $items,
|
'items' => $items,
|
||||||
|
'healthyChecks' => $healthyChecks,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,46 +4,80 @@
|
|||||||
|
|
||||||
namespace App\Filament\Widgets\Dashboard;
|
namespace App\Filament\Widgets\Dashboard;
|
||||||
|
|
||||||
|
use App\Filament\Resources\FindingResource;
|
||||||
use App\Models\Finding;
|
use App\Models\Finding;
|
||||||
|
use App\Models\InventoryItem;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Support\OpsUx\ActiveRuns;
|
use App\Support\OpsUx\ActiveRuns;
|
||||||
use Filament\Facades\Filament;
|
use Filament\Facades\Filament;
|
||||||
use Filament\Widgets\Widget;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Illuminate\Support\Collection;
|
use Filament\Tables\Table;
|
||||||
|
use Filament\Widgets\TableWidget;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
||||||
class RecentDriftFindings extends Widget
|
class RecentDriftFindings extends TableWidget
|
||||||
{
|
{
|
||||||
protected static bool $isLazy = false;
|
protected static bool $isLazy = false;
|
||||||
|
|
||||||
protected string $view = 'filament.widgets.dashboard.recent-drift-findings';
|
public function table(Table $table): Table
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
protected function getViewData(): array
|
|
||||||
{
|
{
|
||||||
$tenant = Filament::getTenant();
|
$tenant = Filament::getTenant();
|
||||||
|
|
||||||
if (! $tenant instanceof Tenant) {
|
return $table
|
||||||
return [
|
->heading('Recent Drift Findings')
|
||||||
'pollingInterval' => null,
|
->query($this->getQuery())
|
||||||
'findings' => collect(),
|
->poll(fn (): ?string => ($tenant instanceof Tenant) && ActiveRuns::existForTenant($tenant) ? '10s' : null)
|
||||||
];
|
->paginated([10])
|
||||||
}
|
->columns([
|
||||||
|
TextColumn::make('short_id')
|
||||||
|
->label('ID')
|
||||||
|
->state(fn (Finding $record): string => '#'.$record->getKey())
|
||||||
|
->copyable()
|
||||||
|
->copyableState(fn (Finding $record): string => (string) $record->getKey()),
|
||||||
|
TextColumn::make('subject_display_name')
|
||||||
|
->label('Subject')
|
||||||
|
->placeholder('—')
|
||||||
|
->limit(40)
|
||||||
|
->tooltip(fn (Finding $record): ?string => $record->subject_display_name ?: null),
|
||||||
|
TextColumn::make('severity')
|
||||||
|
->badge()
|
||||||
|
->color(fn (Finding $record): string => match ($record->severity) {
|
||||||
|
Finding::SEVERITY_HIGH => 'danger',
|
||||||
|
Finding::SEVERITY_MEDIUM => 'warning',
|
||||||
|
default => 'gray',
|
||||||
|
}),
|
||||||
|
TextColumn::make('status')
|
||||||
|
->badge()
|
||||||
|
->color(fn (Finding $record): string => $record->status === Finding::STATUS_NEW ? 'warning' : 'gray'),
|
||||||
|
TextColumn::make('created_at')
|
||||||
|
->label('Created')
|
||||||
|
->since(),
|
||||||
|
])
|
||||||
|
->recordUrl(fn (Finding $record): ?string => $tenant instanceof Tenant
|
||||||
|
? FindingResource::getUrl('view', ['record' => $record], tenant: $tenant)
|
||||||
|
: null)
|
||||||
|
->emptyStateHeading('No drift findings')
|
||||||
|
->emptyStateDescription('You\'re looking good — no drift findings to review yet.');
|
||||||
|
}
|
||||||
|
|
||||||
$tenantId = (int) $tenant->getKey();
|
/**
|
||||||
|
* @return Builder<Finding>
|
||||||
|
*/
|
||||||
|
private function getQuery(): Builder
|
||||||
|
{
|
||||||
|
$tenant = Filament::getTenant();
|
||||||
|
$tenantId = $tenant instanceof Tenant ? $tenant->getKey() : null;
|
||||||
|
|
||||||
/** @var Collection<int, Finding> $findings */
|
return Finding::query()
|
||||||
$findings = Finding::query()
|
->addSelect([
|
||||||
->where('tenant_id', $tenantId)
|
'subject_display_name' => InventoryItem::query()
|
||||||
|
->select('display_name')
|
||||||
|
->whereColumn('inventory_items.tenant_id', 'findings.tenant_id')
|
||||||
|
->whereColumn('inventory_items.external_id', 'findings.subject_external_id')
|
||||||
|
->limit(1),
|
||||||
|
])
|
||||||
|
->when($tenantId, fn (Builder $query) => $query->where('tenant_id', $tenantId))
|
||||||
->where('finding_type', Finding::FINDING_TYPE_DRIFT)
|
->where('finding_type', Finding::FINDING_TYPE_DRIFT)
|
||||||
->latest('created_at')
|
->latest('created_at');
|
||||||
->limit(10)
|
|
||||||
->get();
|
|
||||||
|
|
||||||
return [
|
|
||||||
'pollingInterval' => ActiveRuns::existForTenant($tenant) ? '10s' : null,
|
|
||||||
'findings' => $findings,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,48 +10,85 @@
|
|||||||
use App\Support\OperationRunLinks;
|
use App\Support\OperationRunLinks;
|
||||||
use App\Support\OpsUx\ActiveRuns;
|
use App\Support\OpsUx\ActiveRuns;
|
||||||
use Filament\Facades\Filament;
|
use Filament\Facades\Filament;
|
||||||
use Filament\Widgets\Widget;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Illuminate\Support\Collection;
|
use Filament\Tables\Table;
|
||||||
|
use Filament\Widgets\TableWidget;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
||||||
class RecentOperations extends Widget
|
class RecentOperations extends TableWidget
|
||||||
{
|
{
|
||||||
protected static bool $isLazy = false;
|
protected static bool $isLazy = false;
|
||||||
|
|
||||||
protected string $view = 'filament.widgets.dashboard.recent-operations';
|
|
||||||
|
|
||||||
protected int|string|array $columnSpan = 'full';
|
protected int|string|array $columnSpan = 'full';
|
||||||
|
|
||||||
/**
|
public function table(Table $table): Table
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
protected function getViewData(): array
|
|
||||||
{
|
{
|
||||||
$tenant = Filament::getTenant();
|
$tenant = Filament::getTenant();
|
||||||
|
|
||||||
if (! $tenant instanceof Tenant) {
|
return $table
|
||||||
return [
|
->heading('Recent Operations')
|
||||||
'pollingInterval' => null,
|
->query($this->getQuery())
|
||||||
'runs' => collect(),
|
->poll(fn (): ?string => ($tenant instanceof Tenant) && ActiveRuns::existForTenant($tenant) ? '10s' : null)
|
||||||
'viewRunBaseUrl' => null,
|
->paginated([10])
|
||||||
];
|
->columns([
|
||||||
}
|
TextColumn::make('short_id')
|
||||||
|
->label('Run')
|
||||||
|
->state(fn (OperationRun $record): string => '#'.$record->getKey())
|
||||||
|
->copyable()
|
||||||
|
->copyableState(fn (OperationRun $record): string => (string) $record->getKey()),
|
||||||
|
TextColumn::make('type')
|
||||||
|
->label('Operation')
|
||||||
|
->formatStateUsing(fn (?string $state): string => OperationCatalog::label((string) $state))
|
||||||
|
->limit(40)
|
||||||
|
->tooltip(fn (OperationRun $record): string => OperationCatalog::label((string) $record->type)),
|
||||||
|
TextColumn::make('status')
|
||||||
|
->badge()
|
||||||
|
->color(fn (OperationRun $record): string => $this->statusColor($record->status)),
|
||||||
|
TextColumn::make('outcome')
|
||||||
|
->badge()
|
||||||
|
->color(fn (OperationRun $record): string => $this->outcomeColor($record->outcome)),
|
||||||
|
TextColumn::make('created_at')
|
||||||
|
->label('Started')
|
||||||
|
->since(),
|
||||||
|
])
|
||||||
|
->recordUrl(fn (OperationRun $record): ?string => $tenant instanceof Tenant
|
||||||
|
? OperationRunLinks::view($record, $tenant)
|
||||||
|
: null)
|
||||||
|
->emptyStateHeading('No operations yet')
|
||||||
|
->emptyStateDescription('Once you run inventory sync, drift generation, or restores, they\'ll show up here.');
|
||||||
|
}
|
||||||
|
|
||||||
$tenantId = (int) $tenant->getKey();
|
/**
|
||||||
|
* @return Builder<OperationRun>
|
||||||
|
*/
|
||||||
|
private function getQuery(): Builder
|
||||||
|
{
|
||||||
|
$tenant = Filament::getTenant();
|
||||||
|
$tenantId = $tenant instanceof Tenant ? $tenant->getKey() : null;
|
||||||
|
|
||||||
/** @var Collection<int, OperationRun> $runs */
|
return OperationRun::query()
|
||||||
$runs = OperationRun::query()
|
->when($tenantId, fn (Builder $query) => $query->where('tenant_id', $tenantId))
|
||||||
->where('tenant_id', $tenantId)
|
->latest('created_at');
|
||||||
->latest('created_at')
|
}
|
||||||
->limit(10)
|
|
||||||
->get()
|
|
||||||
->each(function (OperationRun $run) use ($tenant): void {
|
|
||||||
$run->setAttribute('type_label', OperationCatalog::label((string) $run->type));
|
|
||||||
$run->setAttribute('view_url', OperationRunLinks::view($run, $tenant));
|
|
||||||
});
|
|
||||||
|
|
||||||
return [
|
private function statusColor(?string $status): string
|
||||||
'pollingInterval' => ActiveRuns::existForTenant($tenant) ? '10s' : null,
|
{
|
||||||
'runs' => $runs,
|
return match ($status) {
|
||||||
];
|
'queued' => 'secondary',
|
||||||
|
'running' => 'warning',
|
||||||
|
'completed' => 'success',
|
||||||
|
default => 'gray',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function outcomeColor(?string $outcome): string
|
||||||
|
{
|
||||||
|
return match ($outcome) {
|
||||||
|
'succeeded' => 'success',
|
||||||
|
'partially_succeeded' => 'warning',
|
||||||
|
'failed' => 'danger',
|
||||||
|
'cancelled' => 'gray',
|
||||||
|
default => 'gray',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,28 +0,0 @@
|
|||||||
<div
|
|
||||||
@if ($pollingInterval)
|
|
||||||
wire:poll.{{ $pollingInterval }}
|
|
||||||
@endif
|
|
||||||
class="rounded-xl bg-white p-6 shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10"
|
|
||||||
>
|
|
||||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-4">
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<div class="text-sm text-gray-500 dark:text-gray-400">Open drift findings</div>
|
|
||||||
<div class="text-2xl font-semibold text-gray-950 dark:text-white">{{ $openDriftFindings }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<div class="text-sm text-gray-500 dark:text-gray-400">High severity drift</div>
|
|
||||||
<div class="text-2xl font-semibold text-gray-950 dark:text-white">{{ $highSeverityDriftFindings }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<div class="text-sm text-gray-500 dark:text-gray-400">Active operations</div>
|
|
||||||
<div class="text-2xl font-semibold text-gray-950 dark:text-white">{{ $activeRuns }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-1">
|
|
||||||
<div class="text-sm text-gray-500 dark:text-gray-400">Inventory active</div>
|
|
||||||
<div class="text-2xl font-semibold text-gray-950 dark:text-white">{{ $inventoryActiveRuns }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@ -2,25 +2,54 @@
|
|||||||
@if ($pollingInterval)
|
@if ($pollingInterval)
|
||||||
wire:poll.{{ $pollingInterval }}
|
wire:poll.{{ $pollingInterval }}
|
||||||
@endif
|
@endif
|
||||||
class="rounded-xl bg-white p-6 shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10"
|
class="flex flex-col gap-4"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="text-base font-semibold">Needs Attention</div>
|
||||||
<div class="text-base font-semibold text-gray-950 dark:text-white">Needs Attention</div>
|
|
||||||
|
@if (count($items) === 0)
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Everything looks healthy right now.
|
||||||
|
</div>
|
||||||
|
|
||||||
@if (count($items) === 0)
|
|
||||||
<div class="text-sm text-gray-600 dark:text-gray-300">Nothing urgent right now.</div>
|
|
||||||
@else
|
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
@foreach ($items as $item)
|
@foreach ($healthyChecks as $check)
|
||||||
<a
|
<div class="flex items-start gap-3">
|
||||||
href="{{ $item['url'] }}"
|
<x-filament::icon
|
||||||
class="rounded-lg border border-gray-200 bg-gray-50 p-4 text-left transition hover:bg-gray-100 dark:border-white/10 dark:bg-white/5 dark:hover:bg-white/10"
|
icon="heroicon-m-check-circle"
|
||||||
>
|
class="mt-0.5 h-5 w-5 text-success-600 dark:text-success-400"
|
||||||
<div class="text-sm font-semibold text-gray-950 dark:text-white">{{ $item['title'] }}</div>
|
/>
|
||||||
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">{{ $item['body'] }}</div>
|
|
||||||
</a>
|
<div class="flex-1">
|
||||||
|
<div class="text-sm font-medium text-gray-950 dark:text-white">{{ $check['title'] }}</div>
|
||||||
|
<div class="mt-0.5 text-sm text-gray-600 dark:text-gray-300">{{ $check['body'] }}</div>
|
||||||
|
|
||||||
|
<div class="mt-1">
|
||||||
|
<x-filament::link :href="$check['url']" size="sm">
|
||||||
|
{{ $check['linkLabel'] }}
|
||||||
|
</x-filament::link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
@endif
|
</div>
|
||||||
</div>
|
@else
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
@foreach ($items as $item)
|
||||||
|
<a
|
||||||
|
href="{{ $item['url'] }}"
|
||||||
|
class="rounded-lg bg-gray-50 p-4 text-left transition hover:bg-gray-100 dark:bg-white/5 dark:hover:bg-white/10"
|
||||||
|
>
|
||||||
|
<div class="flex items-start justify-between gap-3">
|
||||||
|
<div class="text-sm font-semibold text-gray-950 dark:text-white">{{ $item['title'] }}</div>
|
||||||
|
<x-filament::badge :color="$item['badgeColor']" size="sm">
|
||||||
|
{{ $item['badge'] }}
|
||||||
|
</x-filament::badge>
|
||||||
|
</div>
|
||||||
|
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">{{ $item['body'] }}</div>
|
||||||
|
</a>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,34 +0,0 @@
|
|||||||
<div
|
|
||||||
@if ($pollingInterval)
|
|
||||||
wire:poll.{{ $pollingInterval }}
|
|
||||||
@endif
|
|
||||||
class="rounded-xl bg-white p-6 shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="text-base font-semibold text-gray-950 dark:text-white">Recent Drift Findings</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4">
|
|
||||||
@if ($findings->isEmpty())
|
|
||||||
<div class="text-sm text-gray-600 dark:text-gray-300">No drift findings yet.</div>
|
|
||||||
@else
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
@foreach ($findings as $finding)
|
|
||||||
<div class="rounded-lg border border-gray-200 p-3 dark:border-white/10">
|
|
||||||
<div class="flex items-center justify-between gap-3">
|
|
||||||
<div class="text-sm font-medium text-gray-950 dark:text-white">
|
|
||||||
{{ $finding->subject_type }} · {{ $finding->subject_external_id }}
|
|
||||||
</div>
|
|
||||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
{{ $finding->created_at?->diffForHumans() }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">
|
|
||||||
Severity: {{ $finding->severity }} · Status: {{ $finding->status }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
<div
|
|
||||||
@if ($pollingInterval)
|
|
||||||
wire:poll.{{ $pollingInterval }}
|
|
||||||
@endif
|
|
||||||
class="rounded-xl bg-white p-6 shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="text-base font-semibold text-gray-950 dark:text-white">Recent Operations</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4">
|
|
||||||
@if ($runs->isEmpty())
|
|
||||||
<div class="text-sm text-gray-600 dark:text-gray-300">No operations yet.</div>
|
|
||||||
@else
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
@foreach ($runs as $run)
|
|
||||||
<a
|
|
||||||
href="{{ $run->getAttribute('view_url') }}"
|
|
||||||
class="rounded-lg border border-gray-200 p-3 transition hover:bg-gray-50 dark:border-white/10 dark:hover:bg-white/5"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between gap-3">
|
|
||||||
<div class="text-sm font-medium text-gray-950 dark:text-white">
|
|
||||||
{{ $run->getAttribute('type_label') ?? $run->type }}
|
|
||||||
</div>
|
|
||||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
{{ $run->created_at?->diffForHumans() }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">
|
|
||||||
Status: {{ $run->status }} · Outcome: {{ $run->outcome }}
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
use App\Filament\Pages\TenantDashboard;
|
use App\Filament\Pages\TenantDashboard;
|
||||||
use App\Models\Finding;
|
use App\Models\Finding;
|
||||||
|
use App\Models\InventoryItem;
|
||||||
use App\Models\OperationRun;
|
use App\Models\OperationRun;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
|
|
||||||
@ -11,6 +12,12 @@
|
|||||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||||
$otherTenant = Tenant::factory()->create();
|
$otherTenant = Tenant::factory()->create();
|
||||||
|
|
||||||
|
InventoryItem::factory()->create([
|
||||||
|
'tenant_id' => $otherTenant->getKey(),
|
||||||
|
'external_id' => 'other-tenant-finding',
|
||||||
|
'display_name' => 'Other Tenant Policy',
|
||||||
|
]);
|
||||||
|
|
||||||
Finding::factory()->create([
|
Finding::factory()->create([
|
||||||
'tenant_id' => $otherTenant->getKey(),
|
'tenant_id' => $otherTenant->getKey(),
|
||||||
'finding_type' => Finding::FINDING_TYPE_DRIFT,
|
'finding_type' => Finding::FINDING_TYPE_DRIFT,
|
||||||
@ -29,5 +36,5 @@
|
|||||||
|
|
||||||
$this->get(TenantDashboard::getUrl(tenant: $tenant))
|
$this->get(TenantDashboard::getUrl(tenant: $tenant))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertDontSee('other-tenant-finding');
|
->assertDontSee('Other Tenant Policy');
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user