TenantAtlas/app/Filament/Resources/ProviderConnectionResource.php
ahmido f13a4ce409 feat(110): Ops-UX enterprise start/dedup standard (repo-wide) (#134)
Implements Spec 110 Ops‑UX Enforcement and applies the repo‑wide “enterprise” standard for operation start + dedup surfaces.

Key points
- Start surfaces: only ephemeral queued toast (no DB notifications for started/queued/running).
- Dedup paths: canonical “already queued” toast.
- Progress refresh: dispatch run-enqueued browser event so the global widget updates immediately.
- Completion: exactly-once terminal DB notification on completion (per Ops‑UX contract).

Tests & formatting
- Full suite: 1738 passed, 8 skipped (8477 assertions).
- Pint: `vendor/bin/sail bin pint --dirty --format agent` (pass).

Notable change
- Removed legacy `RunStatusChangedNotification` (replaced by the terminal-only completion notification policy).

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #134
2026-02-24 09:30:15 +00:00

1190 lines
53 KiB
PHP

<?php
namespace App\Filament\Resources;
use App\Filament\Resources\ProviderConnectionResource\Pages;
use App\Jobs\ProviderComplianceSnapshotJob;
use App\Jobs\ProviderInventorySyncJob;
use App\Models\OperationRun;
use App\Models\ProviderConnection;
use App\Models\Tenant;
use App\Models\User;
use App\Services\Auth\CapabilityResolver;
use App\Services\Intune\AuditLogger;
use App\Services\Providers\CredentialManager;
use App\Services\Providers\ProviderOperationStartGate;
use App\Services\Verification\StartVerification;
use App\Support\Auth\Capabilities;
use App\Support\Badges\BadgeDomain;
use App\Support\Badges\BadgeRenderer;
use App\Support\OperationRunLinks;
use App\Support\OpsUx\OperationUxPresenter;
use App\Support\OpsUx\OpsUxBrowserEvents;
use App\Support\Providers\ProviderReasonCodes;
use App\Support\Rbac\UiEnforcement;
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot;
use App\Support\Workspaces\WorkspaceContext;
use BackedEnum;
use Filament\Actions;
use Filament\Facades\Filament;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Notifications\Notification;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Tables;
use Filament\Tables\Filters\Filter;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Str;
use UnitEnum;
class ProviderConnectionResource extends Resource
{
protected static bool $isDiscovered = false;
protected static bool $isScopedToTenant = false;
protected static ?string $model = ProviderConnection::class;
protected static ?string $slug = 'provider-connections';
protected static bool $isGloballySearchable = false;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-link';
protected static string|UnitEnum|null $navigationGroup = 'Settings';
protected static ?string $navigationLabel = 'Provider Connections';
protected static ?string $recordTitleAttribute = 'display_name';
public static function getNavigationParentItem(): ?string
{
return 'Integrations';
}
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
{
return ActionSurfaceDeclaration::forResource(ActionSurfaceProfile::CrudListAndView)
->satisfy(ActionSurfaceSlot::ListHeader, 'Header actions include capability-gated create.')
->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ClickableRow->value)
->satisfy(ActionSurfaceSlot::ListRowMoreMenu, 'Secondary row actions are grouped under "More".')
->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'Provider connections intentionally omit bulk actions.')
->satisfy(ActionSurfaceSlot::ListEmptyState, 'List page defines empty-state guidance and CTA.')
->exempt(ActionSurfaceSlot::DetailHeader, 'View page has no additional header mutations in this resource.');
}
public static function canCreate(): bool
{
$tenant = static::resolveTenantForCreate();
$user = auth()->user();
if (! $tenant instanceof Tenant || ! $user instanceof User) {
return false;
}
/** @var CapabilityResolver $resolver */
$resolver = app(CapabilityResolver::class);
return $resolver->isMember($user, $tenant)
&& $resolver->can($user, $tenant, Capabilities::PROVIDER_MANAGE);
}
protected static function hasTenantCapability(string $capability): bool
{
$tenant = static::resolveScopedTenant();
$user = auth()->user();
if (! $tenant instanceof Tenant || ! $user instanceof User) {
return false;
}
/** @var CapabilityResolver $resolver */
$resolver = app(CapabilityResolver::class);
return $resolver->isMember($user, $tenant)
&& $resolver->can($user, $tenant, $capability);
}
protected static function resolveScopedTenant(): ?Tenant
{
$tenantExternalId = static::resolveRequestedTenantExternalId();
if (is_string($tenantExternalId) && $tenantExternalId !== '') {
return static::resolveTenantByExternalId($tenantExternalId);
}
$routeTenant = request()->route('tenant');
if ($routeTenant instanceof Tenant) {
return $routeTenant;
}
if (is_string($routeTenant) && $routeTenant !== '') {
return Tenant::query()
->where('external_id', $routeTenant)
->first();
}
$recordTenant = static::resolveTenantFromRouteRecord();
if ($recordTenant instanceof Tenant) {
return $recordTenant;
}
$contextTenantExternalId = static::resolveContextTenantExternalId();
if (is_string($contextTenantExternalId) && $contextTenantExternalId !== '') {
return static::resolveTenantByExternalId($contextTenantExternalId);
}
$filamentTenant = Filament::getTenant();
return $filamentTenant instanceof Tenant ? $filamentTenant : null;
}
public static function resolveTenantForRecord(?ProviderConnection $record = null): ?Tenant
{
if ($record instanceof ProviderConnection) {
$tenant = $record->tenant;
if (! $tenant instanceof Tenant && is_numeric($record->tenant_id)) {
$tenant = Tenant::query()->whereKey((int) $record->tenant_id)->first();
}
if ($tenant instanceof Tenant) {
return $tenant;
}
}
return static::resolveScopedTenant();
}
public static function resolveRequestedTenantExternalId(): ?string
{
$queryTenant = request()->query('tenant_id');
if (is_string($queryTenant) && $queryTenant !== '') {
return $queryTenant;
}
return static::resolveTenantExternalIdFromLivewireRequest();
}
public static function resolveContextTenantExternalId(): ?string
{
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());
$contextTenantId = app(WorkspaceContext::class)->lastTenantId(request());
if ($workspaceId !== null && $contextTenantId !== null) {
$tenant = Tenant::query()
->whereKey($contextTenantId)
->where('workspace_id', (int) $workspaceId)
->first();
if ($tenant instanceof Tenant) {
return (string) $tenant->external_id;
}
}
$filamentTenant = Filament::getTenant();
if ($filamentTenant instanceof Tenant) {
return (string) $filamentTenant->external_id;
}
return null;
}
public static function resolveTenantForCreate(): ?Tenant
{
$tenantExternalId = static::resolveRequestedTenantExternalId() ?? static::resolveContextTenantExternalId();
if (! is_string($tenantExternalId) || $tenantExternalId === '') {
return null;
}
$tenant = static::resolveTenantByExternalId($tenantExternalId);
$user = auth()->user();
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());
if (! $tenant instanceof Tenant || ! $user instanceof User || $workspaceId === null) {
return null;
}
if ((int) $tenant->workspace_id !== (int) $workspaceId) {
return null;
}
if (! $user->canAccessTenant($tenant)) {
return null;
}
return $tenant;
}
private static function resolveTenantExternalIdFromLivewireRequest(): ?string
{
if (! request()->headers->has('x-livewire') && ! request()->headers->has('x-livewire-navigate')) {
return null;
}
try {
$url = \Livewire\Livewire::originalUrl();
if (is_string($url) && $url !== '') {
$externalId = static::extractTenantExternalIdFromUrl($url);
if (is_string($externalId) && $externalId !== '') {
return $externalId;
}
}
} catch (\Throwable) {
// Ignore and fall back to referer.
}
$referer = request()->headers->get('referer');
if (! is_string($referer) || $referer === '') {
return null;
}
return static::extractTenantExternalIdFromUrl($referer);
}
private static function extractTenantExternalIdFromUrl(string $url): ?string
{
$query = parse_url($url, PHP_URL_QUERY);
if (is_string($query) && $query !== '') {
parse_str($query, $queryParams);
$tenantExternalId = $queryParams['tenant_id'] ?? null;
if (is_string($tenantExternalId) && $tenantExternalId !== '') {
return $tenantExternalId;
}
}
$path = parse_url($url, PHP_URL_PATH);
if (! is_string($path) || $path === '') {
$path = $url;
}
if (preg_match('~/(?:admin)/(?:tenants|t)/([0-9a-fA-F-]{36})(?:/|$)~', $path, $matches) !== 1) {
return null;
}
return (string) $matches[1];
}
private static function resolveTenantByExternalId(?string $externalId): ?Tenant
{
if (! is_string($externalId) || $externalId === '') {
return null;
}
return Tenant::query()
->where('external_id', $externalId)
->first();
}
private static function resolveTenantFromRouteRecord(): ?Tenant
{
$record = request()->route('record');
if ($record instanceof ProviderConnection) {
return static::resolveTenantForRecord($record);
}
if (! is_numeric($record)) {
return null;
}
$providerConnection = ProviderConnection::query()
->with('tenant')
->whereKey((int) $record)
->first();
if (! $providerConnection instanceof ProviderConnection) {
return null;
}
return static::resolveTenantForRecord($providerConnection);
}
private static function applyMembershipScope(Builder $query): Builder
{
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());
$user = auth()->user();
if (! is_int($workspaceId)) {
$filamentTenant = Filament::getTenant();
if ($filamentTenant instanceof Tenant) {
$workspaceId = (int) $filamentTenant->workspace_id;
}
}
if (! is_int($workspaceId) || ! $user instanceof User) {
return $query->whereRaw('1 = 0');
}
return $query
->where('provider_connections.workspace_id', $workspaceId)
->whereExists(function ($membershipScope) use ($user, $workspaceId): void {
$membershipScope
->selectRaw('1')
->from('tenants as scoped_tenants')
->join('tenant_memberships as scoped_memberships', function (JoinClause $join) use ($user): void {
$join->on('scoped_memberships.tenant_id', '=', 'scoped_tenants.id')
->where('scoped_memberships.user_id', '=', (int) $user->getKey());
})
->whereColumn('scoped_tenants.id', 'provider_connections.tenant_id')
->where('scoped_tenants.workspace_id', '=', $workspaceId);
});
}
/**
* @return array<string, string>
*/
private static function tenantFilterOptions(): array
{
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());
$user = auth()->user();
if (! is_int($workspaceId) || ! $user instanceof User) {
return [];
}
return Tenant::query()
->select(['tenants.external_id', 'tenants.name', 'tenants.environment'])
->join('tenant_memberships as filter_memberships', function (JoinClause $join) use ($user): void {
$join->on('filter_memberships.tenant_id', '=', 'tenants.id')
->where('filter_memberships.user_id', '=', (int) $user->getKey());
})
->where('tenants.workspace_id', $workspaceId)
->orderBy('tenants.name')
->get()
->mapWithKeys(function (Tenant $tenant): array {
$environment = strtoupper((string) ($tenant->environment ?? ''));
$label = $environment !== '' ? "{$tenant->name} ({$environment})" : (string) $tenant->name;
return [(string) $tenant->external_id => $label];
})
->all();
}
private static function sanitizeErrorMessage(?string $value): ?string
{
if (! is_string($value) || trim($value) === '') {
return null;
}
$normalized = preg_replace('/\s+/', ' ', strip_tags($value));
$normalized = is_string($normalized) ? trim($normalized) : '';
if ($normalized === '') {
return null;
}
return Str::limit($normalized, 120);
}
public static function form(Schema $schema): Schema
{
return $schema
->schema([
Section::make('Connection')
->schema([
TextInput::make('display_name')
->label('Display name')
->required()
->disabled(fn (): bool => ! static::hasTenantCapability(Capabilities::PROVIDER_MANAGE))
->maxLength(255),
TextInput::make('entra_tenant_id')
->label('Entra tenant ID')
->required()
->maxLength(255)
->disabled(fn (): bool => ! static::hasTenantCapability(Capabilities::PROVIDER_MANAGE))
->rules(['uuid']),
Toggle::make('is_default')
->label('Default connection')
->disabled(fn (): bool => ! static::hasTenantCapability(Capabilities::PROVIDER_MANAGE))
->helperText('Exactly one default connection is required per tenant/provider.'),
])
->columns(2)
->columnSpanFull(),
Section::make('Status')
->schema([
TextInput::make('status')
->label('Status')
->disabled()
->dehydrated(false),
TextInput::make('health_status')
->label('Health')
->disabled()
->dehydrated(false),
])
->columns(2)
->columnSpanFull(),
]);
}
public static function table(Table $table): Table
{
return $table
->modifyQueryUsing(function (Builder $query): Builder {
$query->with('tenant');
$tenantExternalId = static::resolveRequestedTenantExternalId();
if (! is_string($tenantExternalId) || $tenantExternalId === '') {
return $query;
}
return $query->whereHas('tenant', function (Builder $tenantQuery) use ($tenantExternalId): void {
$tenantQuery->where('external_id', $tenantExternalId);
});
})
->defaultSort('display_name')
->recordUrl(fn (ProviderConnection $record): string => static::getUrl('view', ['record' => $record]))
->columns([
Tables\Columns\TextColumn::make('tenant.name')
->label('Tenant')
->description(function (ProviderConnection $record): ?string {
$environment = $record->tenant?->environment;
if (! is_string($environment) || trim($environment) === '') {
return null;
}
return strtoupper($environment);
})
->url(function (ProviderConnection $record): ?string {
$tenant = static::resolveTenantForRecord($record);
if (! $tenant instanceof Tenant) {
return null;
}
return TenantResource::getUrl('view', ['record' => $tenant], panel: 'admin');
}),
Tables\Columns\TextColumn::make('display_name')->label('Name')->searchable(),
Tables\Columns\TextColumn::make('provider')->label('Provider')->toggleable(),
Tables\Columns\TextColumn::make('entra_tenant_id')->label('Entra tenant ID')->copyable()->toggleable(),
Tables\Columns\IconColumn::make('is_default')->label('Default')->boolean(),
Tables\Columns\TextColumn::make('status')
->label('Status')
->badge()
->formatStateUsing(BadgeRenderer::label(BadgeDomain::ProviderConnectionStatus))
->color(BadgeRenderer::color(BadgeDomain::ProviderConnectionStatus))
->icon(BadgeRenderer::icon(BadgeDomain::ProviderConnectionStatus))
->iconColor(BadgeRenderer::iconColor(BadgeDomain::ProviderConnectionStatus)),
Tables\Columns\TextColumn::make('health_status')
->label('Health')
->badge()
->formatStateUsing(BadgeRenderer::label(BadgeDomain::ProviderConnectionHealth))
->color(BadgeRenderer::color(BadgeDomain::ProviderConnectionHealth))
->icon(BadgeRenderer::icon(BadgeDomain::ProviderConnectionHealth))
->iconColor(BadgeRenderer::iconColor(BadgeDomain::ProviderConnectionHealth)),
Tables\Columns\TextColumn::make('last_health_check_at')->label('Last check')->since()->toggleable(),
Tables\Columns\TextColumn::make('last_error_reason_code')
->label('Last error reason')
->toggleable(),
Tables\Columns\TextColumn::make('last_error_message')
->label('Last error message')
->formatStateUsing(fn (?string $state): ?string => static::sanitizeErrorMessage($state))
->toggleable(),
])
->filters([
SelectFilter::make('tenant')
->label('Tenant')
->default(static::resolveScopedTenant()?->external_id)
->options(static::tenantFilterOptions())
->query(function (Builder $query, array $data): Builder {
$value = $data['value'] ?? null;
if (! is_string($value) || $value === '') {
return $query;
}
return $query->whereHas('tenant', function (Builder $tenantQuery) use ($value): void {
$tenantQuery->where('external_id', $value);
});
}),
SelectFilter::make('provider')
->label('Provider')
->options([
'microsoft' => 'Microsoft',
])
->query(function (Builder $query, array $data): Builder {
$value = $data['value'] ?? null;
if (! is_string($value) || $value === '') {
return $query;
}
return $query->where('provider_connections.provider', $value);
}),
SelectFilter::make('status')
->label('Status')
->options([
'connected' => 'Connected',
'needs_consent' => 'Needs consent',
'error' => 'Error',
'disabled' => 'Disabled',
])
->query(function (Builder $query, array $data): Builder {
$value = $data['value'] ?? null;
if (! is_string($value) || $value === '') {
return $query;
}
return $query->where('provider_connections.status', $value);
}),
SelectFilter::make('health_status')
->label('Health')
->options([
'ok' => 'OK',
'degraded' => 'Degraded',
'down' => 'Down',
'unknown' => 'Unknown',
])
->query(function (Builder $query, array $data): Builder {
$value = $data['value'] ?? null;
if (! is_string($value) || $value === '') {
return $query;
}
return $query->where('provider_connections.health_status', $value);
}),
Filter::make('default_only')
->label('Default only')
->query(fn (Builder $query): Builder => $query->where('provider_connections.is_default', true)),
])
->actions([
Actions\ActionGroup::make([
UiEnforcement::forAction(
Actions\EditAction::make()
)
->requireCapability(Capabilities::PROVIDER_MANAGE)
->apply(),
UiEnforcement::forAction(
Actions\Action::make('check_connection')
->label('Check connection')
->icon('heroicon-o-check-badge')
->color('success')
->visible(fn (ProviderConnection $record): bool => $record->status !== 'disabled')
->action(function (ProviderConnection $record, StartVerification $verification, \Filament\Tables\Contracts\HasTable $livewire): void {
$tenant = static::resolveTenantForRecord($record);
$user = auth()->user();
if (! $tenant instanceof Tenant) {
abort(404);
}
if (! $user instanceof User) {
abort(403);
}
$result = $verification->providerConnectionCheck(
tenant: $tenant,
connection: $record,
initiator: $user,
);
if ($result->status === 'scope_busy') {
Notification::make()
->title('Scope busy')
->body('Another provider operation is already running for this connection.')
->warning()
->actions([
Actions\Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($result->run, $tenant)),
Actions\Action::make('manage_connections')
->label('Manage Provider Connections')
->url(static::getUrl('index', tenant: $tenant)),
])
->send();
return;
}
if ($result->status === 'deduped') {
OpsUxBrowserEvents::dispatchRunEnqueued($livewire);
OperationUxPresenter::alreadyQueuedToast((string) $result->run->type)
->actions([
Actions\Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($result->run, $tenant)),
Actions\Action::make('manage_connections')
->label('Manage Provider Connections')
->url(static::getUrl('index', tenant: $tenant)),
])
->send();
return;
}
if ($result->status === 'blocked') {
$reasonCode = is_string($result->run->context['reason_code'] ?? null)
? (string) $result->run->context['reason_code']
: 'unknown_error';
Notification::make()
->title('Connection check blocked')
->body("Blocked by provider configuration ({$reasonCode}).")
->warning()
->actions([
Actions\Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($result->run, $tenant)),
Actions\Action::make('manage_connections')
->label('Manage Provider Connections')
->url(static::getUrl('index', tenant: $tenant)),
])
->send();
return;
}
OpsUxBrowserEvents::dispatchRunEnqueued($livewire);
OperationUxPresenter::queuedToast((string) $result->run->type)
->actions([
Actions\Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($result->run, $tenant)),
])
->send();
})
)
->preserveVisibility()
->requireCapability(Capabilities::PROVIDER_RUN)
->apply(),
UiEnforcement::forAction(
Actions\Action::make('inventory_sync')
->label('Inventory sync')
->icon('heroicon-o-arrow-path')
->color('info')
->visible(fn (ProviderConnection $record): bool => $record->status !== 'disabled')
->action(function (ProviderConnection $record, ProviderOperationStartGate $gate, \Filament\Tables\Contracts\HasTable $livewire): void {
$tenant = static::resolveTenantForRecord($record);
$user = auth()->user();
if (! $tenant instanceof Tenant || ! $user instanceof User) {
return;
}
$initiator = $user;
$result = $gate->start(
tenant: $tenant,
connection: $record,
operationType: 'inventory_sync',
dispatcher: function (OperationRun $operationRun) use ($tenant, $initiator, $record): void {
ProviderInventorySyncJob::dispatch(
tenantId: (int) $tenant->getKey(),
userId: (int) $initiator->getKey(),
providerConnectionId: (int) $record->getKey(),
operationRun: $operationRun,
);
},
initiator: $initiator,
);
if ($result->status === 'scope_busy') {
Notification::make()
->title('Scope is busy')
->body('Another provider operation is already running for this connection.')
->danger()
->actions([
Actions\Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($result->run, $tenant)),
])
->send();
return;
}
if ($result->status === 'deduped') {
OpsUxBrowserEvents::dispatchRunEnqueued($livewire);
OperationUxPresenter::alreadyQueuedToast((string) $result->run->type)
->actions([
Actions\Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($result->run, $tenant)),
])
->send();
return;
}
if ($result->status === 'blocked') {
$reasonCode = is_string($result->run->context['reason_code'] ?? null)
? (string) $result->run->context['reason_code']
: 'unknown_error';
Notification::make()
->title('Inventory sync blocked')
->body("Blocked by provider configuration ({$reasonCode}).")
->warning()
->actions([
Actions\Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($result->run, $tenant)),
])
->send();
return;
}
OpsUxBrowserEvents::dispatchRunEnqueued($livewire);
OperationUxPresenter::queuedToast((string) $result->run->type)
->actions([
Actions\Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($result->run, $tenant)),
])
->send();
})
)
->preserveVisibility()
->requireCapability(Capabilities::PROVIDER_RUN)
->apply(),
UiEnforcement::forAction(
Actions\Action::make('compliance_snapshot')
->label('Compliance snapshot')
->icon('heroicon-o-shield-check')
->color('info')
->visible(fn (ProviderConnection $record): bool => $record->status !== 'disabled')
->action(function (ProviderConnection $record, ProviderOperationStartGate $gate, \Filament\Tables\Contracts\HasTable $livewire): void {
$tenant = static::resolveTenantForRecord($record);
$user = auth()->user();
if (! $tenant instanceof Tenant || ! $user instanceof User) {
return;
}
$initiator = $user;
$result = $gate->start(
tenant: $tenant,
connection: $record,
operationType: 'compliance.snapshot',
dispatcher: function (OperationRun $operationRun) use ($tenant, $initiator, $record): void {
ProviderComplianceSnapshotJob::dispatch(
tenantId: (int) $tenant->getKey(),
userId: (int) $initiator->getKey(),
providerConnectionId: (int) $record->getKey(),
operationRun: $operationRun,
);
},
initiator: $initiator,
);
if ($result->status === 'scope_busy') {
Notification::make()
->title('Scope is busy')
->body('Another provider operation is already running for this connection.')
->danger()
->actions([
Actions\Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($result->run, $tenant)),
])
->send();
return;
}
if ($result->status === 'deduped') {
OpsUxBrowserEvents::dispatchRunEnqueued($livewire);
OperationUxPresenter::alreadyQueuedToast((string) $result->run->type)
->actions([
Actions\Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($result->run, $tenant)),
])
->send();
return;
}
if ($result->status === 'blocked') {
$reasonCode = is_string($result->run->context['reason_code'] ?? null)
? (string) $result->run->context['reason_code']
: 'unknown_error';
Notification::make()
->title('Compliance snapshot blocked')
->body("Blocked by provider configuration ({$reasonCode}).")
->warning()
->actions([
Actions\Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($result->run, $tenant)),
])
->send();
return;
}
OpsUxBrowserEvents::dispatchRunEnqueued($livewire);
OperationUxPresenter::queuedToast((string) $result->run->type)
->actions([
Actions\Action::make('view_run')
->label('View run')
->url(OperationRunLinks::view($result->run, $tenant)),
])
->send();
})
)
->preserveVisibility()
->requireCapability(Capabilities::PROVIDER_RUN)
->apply(),
UiEnforcement::forAction(
Actions\Action::make('set_default')
->label('Set as default')
->icon('heroicon-o-star')
->color('primary')
->requiresConfirmation()
->visible(fn (ProviderConnection $record): bool => $record->status !== 'disabled' && ! $record->is_default)
->action(function (ProviderConnection $record, AuditLogger $auditLogger): void {
$tenant = static::resolveTenantForRecord($record);
if (! $tenant instanceof Tenant) {
return;
}
$record->makeDefault();
$user = auth()->user();
$actorId = $user instanceof User ? (int) $user->getKey() : null;
$actorEmail = $user instanceof User ? $user->email : null;
$actorName = $user instanceof User ? $user->name : null;
$auditLogger->log(
tenant: $tenant,
action: 'provider_connection.default_set',
context: [
'metadata' => [
'provider' => $record->provider,
'entra_tenant_id' => $record->entra_tenant_id,
],
],
actorId: $actorId,
actorEmail: $actorEmail,
actorName: $actorName,
resourceType: 'provider_connection',
resourceId: (string) $record->getKey(),
status: 'success',
);
Notification::make()
->title('Default connection updated')
->success()
->send();
})
)
->preserveVisibility()
->requireCapability(Capabilities::PROVIDER_MANAGE)
->apply(),
UiEnforcement::forAction(
Actions\Action::make('update_credentials')
->label('Update credentials')
->icon('heroicon-o-key')
->color('primary')
->requiresConfirmation()
->modalDescription('Client secret is stored encrypted and will never be shown again.')
->form([
TextInput::make('client_id')
->label('Client ID')
->required()
->maxLength(255),
TextInput::make('client_secret')
->label('Client secret')
->password()
->required()
->maxLength(255),
])
->action(function (array $data, ProviderConnection $record, CredentialManager $credentials, AuditLogger $auditLogger): void {
$tenant = static::resolveTenantForRecord($record);
if (! $tenant instanceof Tenant) {
return;
}
$credentials->upsertClientSecretCredential(
connection: $record,
clientId: (string) $data['client_id'],
clientSecret: (string) $data['client_secret'],
);
$user = auth()->user();
$actorId = $user instanceof User ? (int) $user->getKey() : null;
$actorEmail = $user instanceof User ? $user->email : null;
$actorName = $user instanceof User ? $user->name : null;
$auditLogger->log(
tenant: $tenant,
action: 'provider_connection.credentials_updated',
context: [
'metadata' => [
'provider' => $record->provider,
'entra_tenant_id' => $record->entra_tenant_id,
'client_id' => (string) $data['client_id'],
],
],
actorId: $actorId,
actorEmail: $actorEmail,
actorName: $actorName,
resourceType: 'provider_connection',
resourceId: (string) $record->getKey(),
status: 'success',
);
Notification::make()
->title('Credentials updated')
->success()
->send();
})
)
->requireCapability(Capabilities::PROVIDER_MANAGE)
->apply(),
UiEnforcement::forAction(
Actions\Action::make('enable_connection')
->label('Enable connection')
->icon('heroicon-o-play')
->color('success')
->requiresConfirmation()
->visible(fn (ProviderConnection $record): bool => $record->status === 'disabled')
->action(function (ProviderConnection $record, AuditLogger $auditLogger): void {
$tenant = static::resolveTenantForRecord($record);
if (! $tenant instanceof Tenant) {
return;
}
$hadCredentials = $record->credential()->exists();
$previousStatus = (string) $record->status;
$status = $hadCredentials ? 'connected' : 'error';
$errorReasonCode = $hadCredentials ? null : ProviderReasonCodes::ProviderCredentialMissing;
$errorMessage = $hadCredentials ? null : 'Provider connection credentials are missing.';
$record->update([
'status' => $status,
'health_status' => 'unknown',
'last_health_check_at' => null,
'last_error_reason_code' => $errorReasonCode,
'last_error_message' => $errorMessage,
]);
$user = auth()->user();
$actorId = $user instanceof User ? (int) $user->getKey() : null;
$actorEmail = $user instanceof User ? $user->email : null;
$actorName = $user instanceof User ? $user->name : null;
$auditLogger->log(
tenant: $tenant,
action: 'provider_connection.enabled',
context: [
'metadata' => [
'provider' => $record->provider,
'entra_tenant_id' => $record->entra_tenant_id,
'from_status' => $previousStatus,
'to_status' => $status,
'credentials_present' => $hadCredentials,
],
],
actorId: $actorId,
actorEmail: $actorEmail,
actorName: $actorName,
resourceType: 'provider_connection',
resourceId: (string) $record->getKey(),
status: 'success',
);
if (! $hadCredentials) {
Notification::make()
->title('Connection enabled (credentials missing)')
->body('Add credentials before running checks or operations.')
->warning()
->send();
return;
}
Notification::make()
->title('Provider connection enabled')
->success()
->send();
})
)
->preserveVisibility()
->requireCapability(Capabilities::PROVIDER_MANAGE)
->apply(),
UiEnforcement::forAction(
Actions\Action::make('disable_connection')
->label('Disable connection')
->icon('heroicon-o-archive-box-x-mark')
->color('danger')
->requiresConfirmation()
->visible(fn (ProviderConnection $record): bool => $record->status !== 'disabled')
->action(function (ProviderConnection $record, AuditLogger $auditLogger): void {
$tenant = static::resolveTenantForRecord($record);
if (! $tenant instanceof Tenant) {
return;
}
$previousStatus = (string) $record->status;
$record->update([
'status' => 'disabled',
]);
$user = auth()->user();
$actorId = $user instanceof User ? (int) $user->getKey() : null;
$actorEmail = $user instanceof User ? $user->email : null;
$actorName = $user instanceof User ? $user->name : null;
$auditLogger->log(
tenant: $tenant,
action: 'provider_connection.disabled',
context: [
'metadata' => [
'provider' => $record->provider,
'entra_tenant_id' => $record->entra_tenant_id,
'from_status' => $previousStatus,
],
],
actorId: $actorId,
actorEmail: $actorEmail,
actorName: $actorName,
resourceType: 'provider_connection',
resourceId: (string) $record->getKey(),
status: 'success',
);
Notification::make()
->title('Provider connection disabled')
->warning()
->send();
})
)
->preserveVisibility()
->requireCapability(Capabilities::PROVIDER_MANAGE)
->apply(),
])
->label('More')
->icon('heroicon-o-ellipsis-vertical')
->color('gray'),
])
->bulkActions([]);
}
public static function getEloquentQuery(): Builder
{
$query = parent::getEloquentQuery()
->with('tenant');
return static::applyMembershipScope($query)
->latest('provider_connections.id');
}
public static function getPages(): array
{
return [
'index' => Pages\ListProviderConnections::route('/'),
'create' => Pages\CreateProviderConnection::route('/create'),
'view' => Pages\ViewProviderConnection::route('/{record}'),
'edit' => Pages\EditProviderConnection::route('/{record}/edit'),
];
}
private static function normalizeTenantExternalId(mixed $tenant): ?string
{
if ($tenant instanceof Tenant) {
return (string) $tenant->external_id;
}
if (is_string($tenant) && $tenant !== '') {
return $tenant;
}
if (is_numeric($tenant)) {
$tenantModel = Tenant::query()->whereKey((int) $tenant)->first();
if ($tenantModel instanceof Tenant) {
return (string) $tenantModel->external_id;
}
}
return null;
}
/**
* @param array<mixed> $parameters
*/
public static function getUrl(?string $name = null, array $parameters = [], bool $isAbsolute = true, ?string $panel = null, ?Model $tenant = null, bool $shouldGuessMissingParameters = false): string
{
$panel ??= 'admin';
$tenantExternalId = null;
if (array_key_exists('tenant', $parameters)) {
$tenantExternalId = static::normalizeTenantExternalId($parameters['tenant']);
unset($parameters['tenant']);
}
if ($tenantExternalId === null && $tenant instanceof Tenant) {
$tenantExternalId = (string) $tenant->external_id;
}
if ($tenantExternalId === null) {
$record = $parameters['record'] ?? null;
if ($record instanceof ProviderConnection) {
$tenantExternalId = static::resolveTenantForRecord($record)?->external_id;
}
}
if ($tenantExternalId === null) {
$tenantExternalId = static::resolveScopedTenant()?->external_id;
}
if (! array_key_exists('tenant_id', $parameters) && is_string($tenantExternalId) && $tenantExternalId !== '') {
$parameters['tenant_id'] = $tenantExternalId;
}
return parent::getUrl($name, $parameters, $isAbsolute, $panel, null, $shouldGuessMissingParameters);
}
}