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';
|
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>
|
* @return Collection<int, Tenant>
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -75,6 +75,18 @@ class ManagedTenantOnboardingWizard extends Page
|
|||||||
|
|
||||||
protected static ?string $slug = 'onboarding';
|
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 Workspace $workspace;
|
||||||
|
|
||||||
public ?Tenant $managedTenant = null;
|
public ?Tenant $managedTenant = null;
|
||||||
|
|||||||
@ -17,7 +17,8 @@ protected function getHeaderActions(): array
|
|||||||
return [
|
return [
|
||||||
CreateAction::make()
|
CreateAction::make()
|
||||||
->label('Create baseline profile')
|
->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\ProviderConnection;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\WorkspaceMembership;
|
|
||||||
use App\Services\Auth\CapabilityResolver;
|
use App\Services\Auth\CapabilityResolver;
|
||||||
use App\Services\Auth\RoleCapabilityMap;
|
use App\Services\Auth\RoleCapabilityMap;
|
||||||
use App\Services\Directory\EntraGroupLabelResolver;
|
use App\Services\Directory\EntraGroupLabelResolver;
|
||||||
@ -76,29 +75,13 @@ class TenantResource extends Resource
|
|||||||
|
|
||||||
protected static string|UnitEnum|null $navigationGroup = 'Settings';
|
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
|
public static function canCreate(): bool
|
||||||
{
|
{
|
||||||
$user = auth()->user();
|
return false;
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function canEdit(Model $record): bool
|
public static function canEdit(Model $record): bool
|
||||||
@ -999,7 +982,6 @@ public static function getPages(): array
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'index' => Pages\ListTenants::route('/'),
|
'index' => Pages\ListTenants::route('/'),
|
||||||
'create' => Pages\CreateTenant::route('/create'),
|
|
||||||
'view' => Pages\ViewTenant::route('/{record}'),
|
'view' => Pages\ViewTenant::route('/{record}'),
|
||||||
'edit' => Pages\EditTenant::route('/{record}/edit'),
|
'edit' => Pages\EditTenant::route('/{record}/edit'),
|
||||||
'memberships' => Pages\ManageTenantMemberships::route('/{record}/memberships'),
|
'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
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Actions\CreateAction::make()
|
Actions\Action::make('add_tenant')
|
||||||
->disabled(fn (): bool => ! TenantResource::canCreate())
|
->label('Add tenant')
|
||||||
->tooltip(fn (): ?string => TenantResource::canCreate() ? null : 'You do not have permission to register tenants.')
|
->icon('heroicon-m-plus')
|
||||||
|
->url(route('admin.onboarding'))
|
||||||
->visible(fn (): bool => $this->getTableRecords()->count() > 0),
|
->visible(fn (): bool => $this->getTableRecords()->count() > 0),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -23,9 +24,10 @@ protected function getHeaderActions(): array
|
|||||||
protected function getTableEmptyStateActions(): array
|
protected function getTableEmptyStateActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
Actions\CreateAction::make()
|
Actions\Action::make('add_tenant')
|
||||||
->disabled(fn (): bool => ! TenantResource::canCreate())
|
->label('Add tenant')
|
||||||
->tooltip(fn (): ?string => TenantResource::canCreate() ? null : 'You do not have permission to register tenants.'),
|
->icon('heroicon-m-plus')
|
||||||
|
->url(route('admin.onboarding')),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -163,7 +163,7 @@ public function panel(Panel $panel): Panel
|
|||||||
)
|
)
|
||||||
->renderHook(
|
->renderHook(
|
||||||
PanelsRenderHook::BODY_END,
|
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)
|
: ((bool) config('tenantpilot.bulk_operations.progress_widget_enabled', true)
|
||||||
? view('livewire.bulk-operation-progress-wrapper')->render()
|
? view('livewire.bulk-operation-progress-wrapper')->render()
|
||||||
|
|||||||
@ -1,60 +1,133 @@
|
|||||||
<x-filament-panels::page>
|
<x-filament-panels::page>
|
||||||
<x-filament::section>
|
@php
|
||||||
<div class="flex flex-col gap-4">
|
$tenants = $this->getTenants();
|
||||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
$workspace = app(\App\Support\Workspaces\WorkspaceContext::class)->currentWorkspace();
|
||||||
Select a tenant to continue.
|
@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>
|
</div>
|
||||||
|
|
||||||
@php
|
<p class="mb-4 text-sm text-gray-500 dark:text-gray-400">Select a tenant to continue.</p>
|
||||||
$tenants = $this->getTenants();
|
|
||||||
@endphp
|
|
||||||
|
|
||||||
@if ($tenants->isEmpty())
|
{{-- Tenant cards --}}
|
||||||
<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="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-{{ min($tenants->count(), 3) }}">
|
||||||
<div class="font-medium text-gray-900 dark:text-gray-100">No tenants are available for your account.</div>
|
@foreach ($tenants as $tenant)
|
||||||
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">
|
<button
|
||||||
Switch workspaces, or contact an administrator.
|
type="button"
|
||||||
</div>
|
wire:key="tenant-{{ $tenant->id }}"
|
||||||
|
wire:click="selectTenant({{ (int) $tenant->id }})"
|
||||||
<div class="mt-4 flex flex-col gap-2 sm:flex-row">
|
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"
|
||||||
<x-filament::button
|
>
|
||||||
type="button"
|
{{-- Loading overlay --}}
|
||||||
color="gray"
|
<div wire:loading wire:target="selectTenant({{ (int) $tenant->id }})"
|
||||||
tag="a"
|
class="absolute inset-0 z-10 flex items-center justify-center rounded-xl bg-white/80 dark:bg-gray-900/80">
|
||||||
href="{{ route('filament.admin.pages.choose-workspace') }}"
|
<x-filament::loading-indicator class="h-5 w-5 text-primary-500" />
|
||||||
>
|
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
|
||||||
</div>
|
<div class="flex items-start gap-3">
|
||||||
@endif
|
<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>
|
</div>
|
||||||
</x-filament::section>
|
@endif
|
||||||
</x-filament-panels::page>
|
</x-filament-panels::page>
|
||||||
|
|||||||
@ -1,170 +1,3 @@
|
|||||||
<x-filament-panels::page>
|
<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>
|
</x-filament-panels::page>
|
||||||
|
|||||||
@ -25,7 +25,7 @@
|
|||||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
||||||
->get('/admin/choose-tenant')
|
->get('/admin/choose-tenant')
|
||||||
->assertSuccessful()
|
->assertSuccessful()
|
||||||
->assertSee('No tenants are available')
|
->assertSee('No tenants available')
|
||||||
->assertDontSee('Register tenant');
|
->assertDontSee('Register tenant');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -44,7 +44,7 @@
|
|||||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
||||||
->get('/admin/choose-tenant')
|
->get('/admin/choose-tenant')
|
||||||
->assertSuccessful()
|
->assertSuccessful()
|
||||||
->assertSee('No tenants are available')
|
->assertSee('No tenants available')
|
||||||
->assertSee('Change workspace')
|
->assertSee('Switch workspace')
|
||||||
->assertDontSee('Register tenant');
|
->assertDontSee('Register tenant');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Filament\Resources\TenantResource\Pages\CreateTenant;
|
|
||||||
use App\Filament\Resources\TenantResource\Pages\ViewTenant;
|
use App\Filament\Resources\TenantResource\Pages\ViewTenant;
|
||||||
use App\Models\OperationRun;
|
use App\Models\OperationRun;
|
||||||
use App\Models\ProviderConnection;
|
use App\Models\ProviderConnection;
|
||||||
@ -18,33 +17,22 @@
|
|||||||
|
|
||||||
uses(RefreshDatabase::class);
|
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();
|
Queue::fake();
|
||||||
bindFailHardGraphClient();
|
bindFailHardGraphClient();
|
||||||
|
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
|
||||||
$contextTenant = Tenant::create([
|
$tenant = Tenant::factory()->create([
|
||||||
'tenant_id' => 'tenant-context',
|
'tenant_id' => 'tenant-guid',
|
||||||
'name' => 'Context Tenant',
|
'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);
|
$this->actingAs($user);
|
||||||
Filament::setTenant($contextTenant, true);
|
Filament::setTenant($tenant, 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();
|
|
||||||
|
|
||||||
$connection = ProviderConnection::factory()->create([
|
$connection = ProviderConnection::factory()->create([
|
||||||
'tenant_id' => (int) $tenant->getKey(),
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Filament\Pages\Tenancy\RegisterTenant;
|
use App\Filament\Pages\Tenancy\RegisterTenant;
|
||||||
use App\Filament\Resources\TenantResource\Pages\CreateTenant;
|
use App\Filament\Resources\TenantResource;
|
||||||
use Filament\Facades\Filament;
|
use Filament\Facades\Filament;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
@ -24,12 +24,10 @@
|
|||||||
Filament::setCurrentPanel(null);
|
Filament::setCurrentPanel(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('readonly users cannot create tenants', function () {
|
test('readonly users cannot create tenants via CRUD', function () {
|
||||||
[$user] = createUserWithTenant(role: 'readonly');
|
[$user] = createUserWithTenant(role: 'readonly');
|
||||||
|
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
|
||||||
Livewire::actingAs($user)
|
expect(TenantResource::canCreate())->toBeFalse();
|
||||||
->test(CreateTenant::class)
|
|
||||||
->assertStatus(403);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -15,12 +15,12 @@
|
|||||||
expect(TenantResource::canCreate())->toBeFalse();
|
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');
|
[$user] = createUserWithTenant(role: 'manager');
|
||||||
|
|
||||||
$this->actingAs($user);
|
$this->actingAs($user);
|
||||||
|
|
||||||
expect(TenantResource::canCreate())->toBeTrue();
|
expect(TenantResource::canCreate())->toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can be edited by managers (TENANT_MANAGE)', function () {
|
it('can be edited by managers (TENANT_MANAGE)', function () {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user