fix: consolidate tenant creation + harden selection flows
This commit is contained in:
parent
88ba8a14d8
commit
0856aeaa72
@ -27,6 +27,17 @@ class ChooseTenant extends Page
|
||||
|
||||
protected string $view = 'filament.pages.choose-tenant';
|
||||
|
||||
/**
|
||||
* Disable the simple-layout topbar to prevent lazy-loaded
|
||||
* DatabaseNotifications from triggering Livewire update 404s.
|
||||
*/
|
||||
protected function getLayoutData(): array
|
||||
{
|
||||
return [
|
||||
'hasTopbar' => false,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Tenant>
|
||||
*/
|
||||
|
||||
@ -75,6 +75,18 @@ class ManagedTenantOnboardingWizard extends Page
|
||||
|
||||
protected static ?string $slug = 'onboarding';
|
||||
|
||||
/**
|
||||
* Disable the simple-layout topbar to prevent lazy-loaded
|
||||
* DatabaseNotifications from triggering Livewire update 404s
|
||||
* on this workspace-scoped route.
|
||||
*/
|
||||
protected function getLayoutData(): array
|
||||
{
|
||||
return [
|
||||
'hasTopbar' => false,
|
||||
];
|
||||
}
|
||||
|
||||
public Workspace $workspace;
|
||||
|
||||
public ?Tenant $managedTenant = null;
|
||||
|
||||
@ -17,7 +17,8 @@ protected function getHeaderActions(): array
|
||||
return [
|
||||
CreateAction::make()
|
||||
->label('Create baseline profile')
|
||||
->disabled(fn (): bool => ! BaselineProfileResource::canCreate()),
|
||||
->disabled(fn (): bool => ! BaselineProfileResource::canCreate())
|
||||
->visible(fn (): bool => $this->getTableRecords()->count() > 0),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,7 +12,6 @@
|
||||
use App\Models\ProviderConnection;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Models\WorkspaceMembership;
|
||||
use App\Services\Auth\CapabilityResolver;
|
||||
use App\Services\Auth\RoleCapabilityMap;
|
||||
use App\Services\Directory\EntraGroupLabelResolver;
|
||||
@ -76,29 +75,13 @@ class TenantResource extends Resource
|
||||
|
||||
protected static string|UnitEnum|null $navigationGroup = 'Settings';
|
||||
|
||||
/**
|
||||
* Tenant creation is handled exclusively by the onboarding wizard.
|
||||
* The CRUD create page has been removed.
|
||||
*/
|
||||
public static function canCreate(): bool
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
if (! $user instanceof User) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (static::userCanManageAnyTenant($user)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId();
|
||||
|
||||
if ($workspaceId === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return WorkspaceMembership::query()
|
||||
->where('workspace_id', $workspaceId)
|
||||
->where('user_id', $user->getKey())
|
||||
->whereIn('role', ['owner', 'manager'])
|
||||
->exists();
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function canEdit(Model $record): bool
|
||||
@ -999,7 +982,6 @@ public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListTenants::route('/'),
|
||||
'create' => Pages\CreateTenant::route('/create'),
|
||||
'view' => Pages\ViewTenant::route('/{record}'),
|
||||
'edit' => Pages\EditTenant::route('/{record}/edit'),
|
||||
'memberships' => Pages\ManageTenantMemberships::route('/{record}/memberships'),
|
||||
|
||||
@ -1,41 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\TenantResource\Pages;
|
||||
|
||||
use App\Filament\Resources\TenantResource;
|
||||
use App\Models\User;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateTenant extends CreateRecord
|
||||
{
|
||||
protected static string $resource = TenantResource::class;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId();
|
||||
|
||||
if ($workspaceId !== null) {
|
||||
$data['workspace_id'] = $workspaceId;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function afterCreate(): void
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
if (! $user instanceof User) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user->tenants()->syncWithoutDetaching([
|
||||
$this->record->getKey() => ['role' => 'owner'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -13,9 +13,10 @@ class ListTenants extends ListRecords
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make()
|
||||
->disabled(fn (): bool => ! TenantResource::canCreate())
|
||||
->tooltip(fn (): ?string => TenantResource::canCreate() ? null : 'You do not have permission to register tenants.')
|
||||
Actions\Action::make('add_tenant')
|
||||
->label('Add tenant')
|
||||
->icon('heroicon-m-plus')
|
||||
->url(route('admin.onboarding'))
|
||||
->visible(fn (): bool => $this->getTableRecords()->count() > 0),
|
||||
];
|
||||
}
|
||||
@ -23,9 +24,10 @@ protected function getHeaderActions(): array
|
||||
protected function getTableEmptyStateActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make()
|
||||
->disabled(fn (): bool => ! TenantResource::canCreate())
|
||||
->tooltip(fn (): ?string => TenantResource::canCreate() ? null : 'You do not have permission to register tenants.'),
|
||||
Actions\Action::make('add_tenant')
|
||||
->label('Add tenant')
|
||||
->icon('heroicon-m-plus')
|
||||
->url(route('admin.onboarding')),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -163,7 +163,7 @@ public function panel(Panel $panel): Panel
|
||||
)
|
||||
->renderHook(
|
||||
PanelsRenderHook::BODY_END,
|
||||
fn (): string => request()->routeIs('admin.workspace.managed-tenants.index')
|
||||
fn (): string => request()->routeIs('admin.workspace.managed-tenants.index', 'admin.onboarding', 'filament.admin.pages.choose-tenant')
|
||||
? ''
|
||||
: ((bool) config('tenantpilot.bulk_operations.progress_widget_enabled', true)
|
||||
? view('livewire.bulk-operation-progress-wrapper')->render()
|
||||
|
||||
@ -1,60 +1,133 @@
|
||||
<x-filament-panels::page>
|
||||
<x-filament::section>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||
Select a tenant to continue.
|
||||
@php
|
||||
$tenants = $this->getTenants();
|
||||
$workspace = app(\App\Support\Workspaces\WorkspaceContext::class)->currentWorkspace();
|
||||
@endphp
|
||||
|
||||
@if ($tenants->isEmpty())
|
||||
{{-- Empty state --}}
|
||||
<div class="mx-auto max-w-md">
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-8 text-center shadow-sm dark:border-white/10 dark:bg-white/5">
|
||||
@if ($workspace)
|
||||
<div class="mb-5 inline-flex items-center gap-1.5 rounded-full border border-gray-200 bg-gray-50 px-3 py-1 text-xs font-medium text-gray-600 dark:border-white/10 dark:bg-white/5 dark:text-gray-400">
|
||||
<x-filament::icon icon="heroicon-m-building-office-2" class="h-3.5 w-3.5" />
|
||||
{{ $workspace->name }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-primary-50 dark:bg-primary-950/30">
|
||||
<x-filament::icon
|
||||
icon="heroicon-o-server-stack"
|
||||
class="h-7 w-7 text-primary-500 dark:text-primary-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 class="text-base font-semibold text-gray-900 dark:text-white">No tenants available</h3>
|
||||
<p class="mx-auto mt-2 max-w-xs text-sm text-gray-500 dark:text-gray-400">
|
||||
There are no active tenants in this workspace yet. Add one via onboarding, or switch to a different workspace.
|
||||
</p>
|
||||
|
||||
<div class="mt-6 flex flex-col items-center gap-3">
|
||||
<x-filament::button
|
||||
tag="a"
|
||||
href="{{ route('admin.onboarding') }}"
|
||||
icon="heroicon-m-plus"
|
||||
size="lg"
|
||||
>
|
||||
Add tenant
|
||||
</x-filament::button>
|
||||
|
||||
<a href="{{ route('filament.admin.pages.choose-workspace') }}"
|
||||
class="inline-flex items-center gap-1.5 text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<x-filament::icon icon="heroicon-m-arrows-right-left" class="h-4 w-4" />
|
||||
Switch workspace
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
{{-- Tenant list --}}
|
||||
<div class="mx-auto max-w-3xl">
|
||||
{{-- Header row --}}
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
@if ($workspace)
|
||||
<div class="inline-flex items-center gap-1.5 rounded-full border border-gray-200 bg-gray-50 px-3 py-1 text-xs font-medium text-gray-600 dark:border-white/10 dark:bg-white/5 dark:text-gray-400">
|
||||
<x-filament::icon icon="heroicon-m-building-office-2" class="h-3.5 w-3.5" />
|
||||
{{ $workspace->name }}
|
||||
</div>
|
||||
@endif
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">
|
||||
· {{ $tenants->count() }} {{ \Illuminate\Support\Str::plural('tenant', $tenants->count()) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@php
|
||||
$tenants = $this->getTenants();
|
||||
@endphp
|
||||
<p class="mb-4 text-sm text-gray-500 dark:text-gray-400">Select a tenant to continue.</p>
|
||||
|
||||
@if ($tenants->isEmpty())
|
||||
<div class="rounded-md border border-gray-200 bg-gray-50 p-4 text-sm text-gray-700 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-200">
|
||||
<div class="font-medium text-gray-900 dark:text-gray-100">No tenants are available for your account.</div>
|
||||
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">
|
||||
Switch workspaces, or contact an administrator.
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex flex-col gap-2 sm:flex-row">
|
||||
<x-filament::button
|
||||
type="button"
|
||||
color="gray"
|
||||
tag="a"
|
||||
href="{{ route('filament.admin.pages.choose-workspace') }}"
|
||||
>
|
||||
Change workspace
|
||||
</x-filament::button>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
@foreach ($tenants as $tenant)
|
||||
<div
|
||||
wire:key="tenant-{{ $tenant->id }}"
|
||||
x-data
|
||||
@click="if ($event.target.closest('button,a,input,select,textarea')) return; $refs.form.submit();"
|
||||
class="cursor-pointer rounded-lg border border-gray-200 p-4 dark:border-gray-800"
|
||||
>
|
||||
<form x-ref="form" method="POST" action="{{ route('admin.select-tenant') }}" class="flex flex-col gap-3">
|
||||
@csrf
|
||||
<input type="hidden" name="tenant_id" value="{{ (int) $tenant->id }}" />
|
||||
<div class="font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ $tenant->name }}
|
||||
</div>
|
||||
|
||||
<x-filament::button
|
||||
type="submit"
|
||||
color="primary"
|
||||
class="w-full"
|
||||
>
|
||||
Continue
|
||||
</x-filament::button>
|
||||
</form>
|
||||
{{-- Tenant cards --}}
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-{{ min($tenants->count(), 3) }}">
|
||||
@foreach ($tenants as $tenant)
|
||||
<button
|
||||
type="button"
|
||||
wire:key="tenant-{{ $tenant->id }}"
|
||||
wire:click="selectTenant({{ (int) $tenant->id }})"
|
||||
class="group relative flex flex-col rounded-xl border border-gray-200 bg-white p-5 text-left shadow-sm transition-all duration-150 hover:border-gray-300 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:border-white/10 dark:bg-white/5 dark:hover:border-white/20 dark:focus:ring-offset-gray-900"
|
||||
>
|
||||
{{-- Loading overlay --}}
|
||||
<div wire:loading wire:target="selectTenant({{ (int) $tenant->id }})"
|
||||
class="absolute inset-0 z-10 flex items-center justify-center rounded-xl bg-white/80 dark:bg-gray-900/80">
|
||||
<x-filament::loading-indicator class="h-5 w-5 text-primary-500" />
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-gray-100 group-hover:bg-gray-200 dark:bg-white/10 dark:group-hover:bg-white/15">
|
||||
<x-filament::icon
|
||||
icon="heroicon-o-server-stack"
|
||||
class="h-5 w-5 text-gray-500 group-hover:text-gray-600 dark:text-gray-400 dark:group-hover:text-gray-300"
|
||||
/>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<h3 class="truncate text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{{ $tenant->name }}
|
||||
</h3>
|
||||
@if ($tenant->domain)
|
||||
<p class="mt-0.5 truncate text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ $tenant->domain }}
|
||||
</p>
|
||||
@endif
|
||||
@if ($tenant->environment)
|
||||
<span class="mt-1.5 inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-600 dark:bg-white/10 dark:text-gray-400">
|
||||
{{ strtoupper($tenant->environment) }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Hover arrow --}}
|
||||
<div class="absolute right-4 top-5 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
<x-filament::icon
|
||||
icon="heroicon-m-arrow-right"
|
||||
class="h-4 w-4 text-gray-400 dark:text-gray-500"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
{{-- Footer links --}}
|
||||
<div class="mt-6 flex items-center justify-center gap-6">
|
||||
<a href="{{ route('admin.onboarding') }}"
|
||||
class="inline-flex items-center gap-1.5 text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<x-filament::icon icon="heroicon-m-plus" class="h-4 w-4" />
|
||||
Add tenant
|
||||
</a>
|
||||
<a href="{{ route('filament.admin.pages.choose-workspace') }}"
|
||||
class="inline-flex items-center gap-1.5 text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<x-filament::icon icon="heroicon-m-arrows-right-left" class="h-4 w-4" />
|
||||
Switch workspace
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
@endif
|
||||
</x-filament-panels::page>
|
||||
|
||||
@ -1,170 +1,3 @@
|
||||
<x-filament-panels::page>
|
||||
<x-filament::section>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||
Workspace: <span class="font-medium text-gray-900 dark:text-gray-100">{{ $this->workspace->name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border border-gray-200 bg-gray-50 p-4 text-sm text-gray-700 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-200">
|
||||
<div class="font-medium text-gray-900 dark:text-gray-100">
|
||||
Managed tenant onboarding
|
||||
</div>
|
||||
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">
|
||||
This wizard will guide you through identifying a managed tenant and verifying access.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($this->managedTenant)
|
||||
<div class="rounded-md border border-gray-200 bg-white p-4 text-sm text-gray-700 dark:border-gray-800 dark:bg-gray-950 dark:text-gray-200">
|
||||
<div class="font-medium text-gray-900 dark:text-gray-100">Identified tenant</div>
|
||||
<dl class="mt-3 grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<dt class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">Name</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ $this->managedTenant->name }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">Tenant ID</dt>
|
||||
<dd class="mt-1 font-mono text-sm text-gray-900 dark:text-gray-100">{{ $this->managedTenant->tenant_id }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@php
|
||||
$verificationSucceeded = $this->verificationSucceeded();
|
||||
$hasTenant = (bool) $this->managedTenant;
|
||||
$hasConnection = $hasTenant && is_int($this->selectedProviderConnectionId) && $this->selectedProviderConnectionId > 0;
|
||||
@endphp
|
||||
|
||||
<div class="grid grid-cols-1 gap-3 lg:grid-cols-2">
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-gray-950">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">Step 1 — Identify managed tenant</div>
|
||||
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">Provide tenant ID + display name to start or resume the flow.</div>
|
||||
</div>
|
||||
<div class="text-xs font-medium {{ $hasTenant ? 'text-emerald-700 dark:text-emerald-400' : 'text-gray-500 dark:text-gray-400' }}">
|
||||
{{ $hasTenant ? 'Done' : 'Pending' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex flex-col gap-2 sm:flex-row">
|
||||
<x-filament::button
|
||||
type="button"
|
||||
color="primary"
|
||||
wire:click="mountAction('identifyManagedTenant')"
|
||||
>
|
||||
{{ $hasTenant ? 'Change tenant' : 'Identify tenant' }}
|
||||
</x-filament::button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-gray-950">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">Step 2 — Provider connection</div>
|
||||
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">Create or pick the connection used to verify access.</div>
|
||||
</div>
|
||||
<div class="text-xs font-medium {{ $hasConnection ? 'text-emerald-700 dark:text-emerald-400' : 'text-gray-500 dark:text-gray-400' }}">
|
||||
{{ $hasConnection ? 'Selected' : ($hasTenant ? 'Pending' : 'Locked') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($hasTenant)
|
||||
<div class="mt-3 text-sm text-gray-700 dark:text-gray-200">
|
||||
<span class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">Selected connection ID</span>
|
||||
<div class="mt-1 font-mono">{{ $this->selectedProviderConnectionId ?? '—' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex flex-col gap-2 sm:flex-row">
|
||||
<x-filament::button
|
||||
type="button"
|
||||
color="gray"
|
||||
wire:click="mountAction('createProviderConnection')"
|
||||
>
|
||||
Create connection
|
||||
</x-filament::button>
|
||||
|
||||
<x-filament::button
|
||||
type="button"
|
||||
color="gray"
|
||||
wire:click="mountAction('selectProviderConnection')"
|
||||
>
|
||||
Select connection
|
||||
</x-filament::button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-gray-950">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">Step 3 — Verify access</div>
|
||||
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">Runs a verification operation and records the result.</div>
|
||||
</div>
|
||||
<div class="text-xs font-medium {{ $verificationSucceeded ? 'text-emerald-700 dark:text-emerald-400' : 'text-gray-500 dark:text-gray-400' }}">
|
||||
{{ $verificationSucceeded ? 'Succeeded' : ($hasConnection ? 'Pending' : 'Locked') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex flex-col gap-2 sm:flex-row">
|
||||
<x-filament::button
|
||||
type="button"
|
||||
color="primary"
|
||||
:disabled="! $hasConnection"
|
||||
wire:click="mountAction('startVerification')"
|
||||
>
|
||||
Run verification
|
||||
</x-filament::button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-gray-950">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">Step 4 — Bootstrap (optional)</div>
|
||||
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">Start inventory/compliance sync after verification.</div>
|
||||
</div>
|
||||
<div class="text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||
{{ $verificationSucceeded ? 'Available' : 'Locked' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex flex-col gap-2 sm:flex-row">
|
||||
<x-filament::button
|
||||
type="button"
|
||||
color="gray"
|
||||
:disabled="! $verificationSucceeded"
|
||||
wire:click="mountAction('startBootstrap')"
|
||||
>
|
||||
Start bootstrap
|
||||
</x-filament::button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-gray-950">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">Step 5 — Complete onboarding</div>
|
||||
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">Marks the tenant as active after successful verification.</div>
|
||||
</div>
|
||||
<div class="text-xs font-medium {{ $verificationSucceeded ? 'text-emerald-700 dark:text-emerald-400' : 'text-gray-500 dark:text-gray-400' }}">
|
||||
{{ $verificationSucceeded ? 'Ready' : 'Locked' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex flex-col gap-2 sm:flex-row">
|
||||
<x-filament::button
|
||||
type="button"
|
||||
color="success"
|
||||
:disabled="! $verificationSucceeded"
|
||||
wire:click="mountAction('completeOnboarding')"
|
||||
>
|
||||
Complete onboarding
|
||||
</x-filament::button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
</x-filament-panels::page>
|
||||
|
||||
@ -25,7 +25,7 @@
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
||||
->get('/admin/choose-tenant')
|
||||
->assertSuccessful()
|
||||
->assertSee('No tenants are available')
|
||||
->assertSee('No tenants available')
|
||||
->assertDontSee('Register tenant');
|
||||
});
|
||||
|
||||
@ -44,7 +44,7 @@
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
||||
->get('/admin/choose-tenant')
|
||||
->assertSuccessful()
|
||||
->assertSee('No tenants are available')
|
||||
->assertSee('Change workspace')
|
||||
->assertSee('No tenants available')
|
||||
->assertSee('Switch workspace')
|
||||
->assertDontSee('Register tenant');
|
||||
});
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
<?php
|
||||
|
||||
use App\Filament\Resources\TenantResource\Pages\CreateTenant;
|
||||
use App\Filament\Resources\TenantResource\Pages\ViewTenant;
|
||||
use App\Models\OperationRun;
|
||||
use App\Models\ProviderConnection;
|
||||
@ -18,33 +17,22 @@
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
test('tenant can be created via filament and verification start enqueues an operation run', function () {
|
||||
test('verification start enqueues an operation run for a tenant', function () {
|
||||
Queue::fake();
|
||||
bindFailHardGraphClient();
|
||||
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$contextTenant = Tenant::create([
|
||||
'tenant_id' => 'tenant-context',
|
||||
'name' => 'Context Tenant',
|
||||
$tenant = Tenant::factory()->create([
|
||||
'tenant_id' => 'tenant-guid',
|
||||
'name' => 'Contoso',
|
||||
'environment' => 'other',
|
||||
'domain' => 'contoso.com',
|
||||
]);
|
||||
[$user, $contextTenant] = createUserWithTenant($contextTenant, $user, role: 'owner');
|
||||
[$user, $tenant] = createUserWithTenant($tenant, $user, role: 'owner', ensureDefaultMicrosoftProviderConnection: false);
|
||||
$this->actingAs($user);
|
||||
Filament::setTenant($contextTenant, true);
|
||||
|
||||
Livewire::test(CreateTenant::class)
|
||||
->fillForm([
|
||||
'name' => 'Contoso',
|
||||
'environment' => 'other',
|
||||
'tenant_id' => 'tenant-guid',
|
||||
'domain' => 'contoso.com',
|
||||
])
|
||||
->call('create')
|
||||
->assertHasNoFormErrors();
|
||||
|
||||
$tenant = Tenant::query()->where('tenant_id', 'tenant-guid')->first();
|
||||
expect($tenant)->not->toBeNull();
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
$connection = ProviderConnection::factory()->create([
|
||||
'tenant_id' => (int) $tenant->getKey(),
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Filament\Pages\Tenancy\RegisterTenant;
|
||||
use App\Filament\Resources\TenantResource\Pages\CreateTenant;
|
||||
use App\Filament\Resources\TenantResource;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
@ -24,12 +24,10 @@
|
||||
Filament::setCurrentPanel(null);
|
||||
});
|
||||
|
||||
test('readonly users cannot create tenants', function () {
|
||||
test('readonly users cannot create tenants via CRUD', function () {
|
||||
[$user] = createUserWithTenant(role: 'readonly');
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(CreateTenant::class)
|
||||
->assertStatus(403);
|
||||
expect(TenantResource::canCreate())->toBeFalse();
|
||||
});
|
||||
|
||||
@ -15,12 +15,12 @@
|
||||
expect(TenantResource::canCreate())->toBeFalse();
|
||||
});
|
||||
|
||||
it('can be created by managers (TENANT_MANAGE)', function () {
|
||||
it('cannot be created via CRUD (onboarding wizard is the only path)', function () {
|
||||
[$user] = createUserWithTenant(role: 'manager');
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
expect(TenantResource::canCreate())->toBeTrue();
|
||||
expect(TenantResource::canCreate())->toBeFalse();
|
||||
});
|
||||
|
||||
it('can be edited by managers (TENANT_MANAGE)', function () {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user