diff --git a/app/Filament/Pages/ChooseTenant.php b/app/Filament/Pages/ChooseTenant.php index f4bedc3..ae2b0eb 100644 --- a/app/Filament/Pages/ChooseTenant.php +++ b/app/Filament/Pages/ChooseTenant.php @@ -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 */ diff --git a/app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php b/app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php index e916043..c6b62e9 100644 --- a/app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php +++ b/app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php @@ -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; diff --git a/app/Filament/Resources/BaselineProfileResource/Pages/ListBaselineProfiles.php b/app/Filament/Resources/BaselineProfileResource/Pages/ListBaselineProfiles.php index 7abf445..4b61dcb 100644 --- a/app/Filament/Resources/BaselineProfileResource/Pages/ListBaselineProfiles.php +++ b/app/Filament/Resources/BaselineProfileResource/Pages/ListBaselineProfiles.php @@ -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), ]; } } diff --git a/app/Filament/Resources/TenantResource.php b/app/Filament/Resources/TenantResource.php index 3103cd7..bef43b1 100644 --- a/app/Filament/Resources/TenantResource.php +++ b/app/Filament/Resources/TenantResource.php @@ -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'), diff --git a/app/Filament/Resources/TenantResource/Pages/CreateTenant.php b/app/Filament/Resources/TenantResource/Pages/CreateTenant.php deleted file mode 100644 index c5fec3e..0000000 --- a/app/Filament/Resources/TenantResource/Pages/CreateTenant.php +++ /dev/null @@ -1,41 +0,0 @@ - $data - * @return array - */ - 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'], - ]); - } -} diff --git a/app/Filament/Resources/TenantResource/Pages/ListTenants.php b/app/Filament/Resources/TenantResource/Pages/ListTenants.php index ebb01d8..6f69899 100644 --- a/app/Filament/Resources/TenantResource/Pages/ListTenants.php +++ b/app/Filament/Resources/TenantResource/Pages/ListTenants.php @@ -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')), ]; } } diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index fa93f01..3ee9abf 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -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() diff --git a/resources/views/filament/pages/choose-tenant.blade.php b/resources/views/filament/pages/choose-tenant.blade.php index fc2cda6..843240e 100644 --- a/resources/views/filament/pages/choose-tenant.blade.php +++ b/resources/views/filament/pages/choose-tenant.blade.php @@ -1,60 +1,133 @@ - -
-
- Select a tenant to continue. + @php + $tenants = $this->getTenants(); + $workspace = app(\App\Support\Workspaces\WorkspaceContext::class)->currentWorkspace(); + @endphp + + @if ($tenants->isEmpty()) + {{-- Empty state --}} +
+
+ @if ($workspace) +
+ + {{ $workspace->name }} +
+ @endif + +
+ +
+ +

No tenants available

+

+ There are no active tenants in this workspace yet. Add one via onboarding, or switch to a different workspace. +

+ +
+ + Add tenant + + + + + Switch workspace + +
+
+
+ @else + {{-- Tenant list --}} +
+ {{-- Header row --}} +
+
+ @if ($workspace) +
+ + {{ $workspace->name }} +
+ @endif + + · {{ $tenants->count() }} {{ \Illuminate\Support\Str::plural('tenant', $tenants->count()) }} + +
- @php - $tenants = $this->getTenants(); - @endphp +

Select a tenant to continue.

- @if ($tenants->isEmpty()) -
-
No tenants are available for your account.
-
- Switch workspaces, or contact an administrator. -
- -
- - Change workspace - -
-
- @else -
- @foreach ($tenants as $tenant) -
-
- @csrf - -
- {{ $tenant->name }} -
- - - Continue - -
+ {{-- Tenant cards --}} +
+ @foreach ($tenants as $tenant) +
- @endif + +
+
+ +
+
+

+ {{ $tenant->name }} +

+ @if ($tenant->domain) +

+ {{ $tenant->domain }} +

+ @endif + @if ($tenant->environment) + + {{ strtoupper($tenant->environment) }} + + @endif +
+
+ + {{-- Hover arrow --}} +
+ +
+ + @endforeach +
+ + {{-- Footer links --}} +
- + @endif diff --git a/resources/views/filament/pages/workspaces/managed-tenant-onboarding-wizard.blade.php b/resources/views/filament/pages/workspaces/managed-tenant-onboarding-wizard.blade.php index b786d58..cc61477 100644 --- a/resources/views/filament/pages/workspaces/managed-tenant-onboarding-wizard.blade.php +++ b/resources/views/filament/pages/workspaces/managed-tenant-onboarding-wizard.blade.php @@ -1,170 +1,3 @@ - -
-
- Workspace: {{ $this->workspace->name }} -
-
-
- Managed tenant onboarding -
-
- This wizard will guide you through identifying a managed tenant and verifying access. -
-
- - @if ($this->managedTenant) -
-
Identified tenant
-
-
-
Name
-
{{ $this->managedTenant->name }}
-
-
-
Tenant ID
-
{{ $this->managedTenant->tenant_id }}
-
-
-
- @endif - - @php - $verificationSucceeded = $this->verificationSucceeded(); - $hasTenant = (bool) $this->managedTenant; - $hasConnection = $hasTenant && is_int($this->selectedProviderConnectionId) && $this->selectedProviderConnectionId > 0; - @endphp - -
-
-
-
-
Step 1 — Identify managed tenant
-
Provide tenant ID + display name to start or resume the flow.
-
-
- {{ $hasTenant ? 'Done' : 'Pending' }} -
-
- -
- - {{ $hasTenant ? 'Change tenant' : 'Identify tenant' }} - -
-
- -
-
-
-
Step 2 — Provider connection
-
Create or pick the connection used to verify access.
-
-
- {{ $hasConnection ? 'Selected' : ($hasTenant ? 'Pending' : 'Locked') }} -
-
- - @if ($hasTenant) -
- Selected connection ID -
{{ $this->selectedProviderConnectionId ?? '—' }}
-
- -
- - Create connection - - - - Select connection - -
- @endif -
- -
-
-
-
Step 3 — Verify access
-
Runs a verification operation and records the result.
-
-
- {{ $verificationSucceeded ? 'Succeeded' : ($hasConnection ? 'Pending' : 'Locked') }} -
-
- -
- - Run verification - -
-
- -
-
-
-
Step 4 — Bootstrap (optional)
-
Start inventory/compliance sync after verification.
-
-
- {{ $verificationSucceeded ? 'Available' : 'Locked' }} -
-
- -
- - Start bootstrap - -
-
-
- -
-
-
-
Step 5 — Complete onboarding
-
Marks the tenant as active after successful verification.
-
-
- {{ $verificationSucceeded ? 'Ready' : 'Locked' }} -
-
- -
- - Complete onboarding - -
-
-
-
diff --git a/tests/Feature/Filament/ChooseTenantEmptyStateRegisterTenantCtaVisibilityTest.php b/tests/Feature/Filament/ChooseTenantEmptyStateRegisterTenantCtaVisibilityTest.php index e259f06..fed24c8 100644 --- a/tests/Feature/Filament/ChooseTenantEmptyStateRegisterTenantCtaVisibilityTest.php +++ b/tests/Feature/Filament/ChooseTenantEmptyStateRegisterTenantCtaVisibilityTest.php @@ -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'); }); diff --git a/tests/Feature/Filament/TenantSetupTest.php b/tests/Feature/Filament/TenantSetupTest.php index fc38a21..5126d3b 100644 --- a/tests/Feature/Filament/TenantSetupTest.php +++ b/tests/Feature/Filament/TenantSetupTest.php @@ -1,6 +1,5 @@ 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(), diff --git a/tests/Feature/Rbac/TenantAdminAuthorizationTest.php b/tests/Feature/Rbac/TenantAdminAuthorizationTest.php index 94bb9f1..d8b7db0 100644 --- a/tests/Feature/Rbac/TenantAdminAuthorizationTest.php +++ b/tests/Feature/Rbac/TenantAdminAuthorizationTest.php @@ -1,7 +1,7 @@ actingAs($user); - Livewire::actingAs($user) - ->test(CreateTenant::class) - ->assertStatus(403); + expect(TenantResource::canCreate())->toBeFalse(); }); diff --git a/tests/Feature/Rbac/TenantResourceAuthorizationTest.php b/tests/Feature/Rbac/TenantResourceAuthorizationTest.php index 2a46054..023ffb9 100644 --- a/tests/Feature/Rbac/TenantResourceAuthorizationTest.php +++ b/tests/Feature/Rbac/TenantResourceAuthorizationTest.php @@ -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 () {