- Implemented the `show` page for managing URL apps with functionalities like updating name, logo, access URL, and connection status. - Added `UrlAppViewTest` for feature testing, ensuring role-based access, and validation of edit actions. - Updated navigation to route URL apps to their respective management views. - Enhanced `OidcService` to handle updated app settings and redirect URIs.
348 lines
15 KiB
PHP
348 lines
15 KiB
PHP
<?php
|
|
|
|
use App\Concerns\HandlesOperations;
|
|
use App\Enums\Permissions\AppPermissionEnum;
|
|
use App\Services\ConnectedAppService;
|
|
use App\Services\ApplicationLogoUploader;
|
|
use App\Data\ConnectedApp\ConnectAppRequest;
|
|
use App\Enums\ConnectionStatusEnum;
|
|
use App\Models\{ConnectedApp, ConnectionStatus};
|
|
use Livewire\Component;
|
|
use Livewire\Attributes\Title;
|
|
use Livewire\WithFileUploads;
|
|
|
|
new #[Title('URL Application Details')]
|
|
class extends Component {
|
|
use HandlesOperations, WithFileUploads;
|
|
|
|
public int $appId;
|
|
public ConnectedApp $app;
|
|
public string $selectedTab = 'general-tab';
|
|
|
|
// Edit fields
|
|
public bool $editGeneralModal = false;
|
|
public string $editName = '';
|
|
public string $editAccessUrl = '';
|
|
public $editLogo = null;
|
|
public bool $editAccessUrlMode = false;
|
|
|
|
private ConnectedAppService $appService;
|
|
private ApplicationLogoUploader $logoUploader;
|
|
|
|
public function boot(ConnectedAppService $service, ApplicationLogoUploader $logoUploader): void
|
|
{
|
|
$this->appService = $service;
|
|
$this->logoUploader = $logoUploader;
|
|
}
|
|
|
|
public function mount(int $id): void
|
|
{
|
|
$this->authorize(AppPermissionEnum::Read->value);
|
|
$this->appId = $id;
|
|
$this->loadAppData();
|
|
}
|
|
|
|
public function loadAppData(): void
|
|
{
|
|
$this->app = ConnectedApp::with(['protocol', 'status', 'roles'])->findOrFail($this->appId);
|
|
}
|
|
|
|
public function changeStatus(string $statusName): void
|
|
{
|
|
$this->authorize(AppPermissionEnum::Update->value);
|
|
|
|
$this->attempt(
|
|
action: function () use ($statusName) {
|
|
$statusId = ConnectionStatus::where('name', $statusName)->firstOrFail()->id;
|
|
$request = new ConnectAppRequest(
|
|
name: $this->app->name,
|
|
connectionProtocolId: $this->app->connection_protocol_id,
|
|
connectionProviderId: $this->app->connection_provider_id,
|
|
slug: $this->app->slug,
|
|
connectionStatusId: $statusId,
|
|
settings: $this->app->settings,
|
|
logo: $this->app->logo,
|
|
);
|
|
$this->appService->update($this->app->id, $request);
|
|
$this->loadAppData();
|
|
},
|
|
successMessage: 'Application status updated successfully!'
|
|
);
|
|
}
|
|
|
|
public function goBack(): void
|
|
{
|
|
$this->redirectRoute('apps.index', navigate: true);
|
|
}
|
|
|
|
public function openEditGeneralModal(): void
|
|
{
|
|
$this->editName = $this->app->name;
|
|
$this->editLogo = null;
|
|
$this->editGeneralModal = true;
|
|
}
|
|
|
|
public function saveGeneralInfo(): void
|
|
{
|
|
$this->authorize(AppPermissionEnum::Update->value);
|
|
|
|
$this->validate([
|
|
'editName' => 'required|string|max:255',
|
|
'editLogo' => 'nullable|image|max:2048',
|
|
]);
|
|
|
|
$this->attempt(
|
|
action: function () {
|
|
$logoPath = $this->editLogo
|
|
? $this->logoUploader->store($this->editLogo)
|
|
: $this->app->logo;
|
|
|
|
$request = new ConnectAppRequest(
|
|
name: $this->editName,
|
|
connectionProtocolId: $this->app->connection_protocol_id,
|
|
connectionProviderId: $this->app->connection_provider_id,
|
|
slug: \Illuminate\Support\Str::slug($this->editName),
|
|
connectionStatusId: $this->app->connection_status_id,
|
|
settings: $this->app->settings,
|
|
logo: $logoPath,
|
|
);
|
|
|
|
$this->appService->update($this->app->id, $request);
|
|
|
|
$this->editGeneralModal = false;
|
|
$this->success('Application updated successfully!');
|
|
$this->loadAppData();
|
|
}
|
|
);
|
|
}
|
|
|
|
public function startEditAccessUrl(): void
|
|
{
|
|
$this->editAccessUrl = data_get($this->app, 'settings.access_url', '');
|
|
$this->editAccessUrlMode = true;
|
|
}
|
|
|
|
public function cancelEditAccessUrl(): void
|
|
{
|
|
$this->editAccessUrlMode = false;
|
|
}
|
|
|
|
public function saveAccessUrl(): void
|
|
{
|
|
$this->authorize(AppPermissionEnum::Update->value);
|
|
|
|
$this->validate([
|
|
'editAccessUrl' => 'required|url|max:255',
|
|
]);
|
|
|
|
$this->attempt(
|
|
action: function () {
|
|
$settings = $this->app->settings ?? [];
|
|
$settings['access_url'] = $this->editAccessUrl;
|
|
|
|
$request = new ConnectAppRequest(
|
|
name: $this->app->name,
|
|
connectionProtocolId: $this->app->connection_protocol_id,
|
|
connectionProviderId: $this->app->connection_provider_id,
|
|
slug: $this->app->slug,
|
|
connectionStatusId: $this->app->connection_status_id,
|
|
settings: $settings,
|
|
logo: $this->app->logo,
|
|
);
|
|
|
|
$this->appService->update($this->app->id, $request);
|
|
|
|
$this->editAccessUrlMode = false;
|
|
$this->success('Access URL updated successfully!');
|
|
$this->loadAppData();
|
|
}
|
|
);
|
|
}
|
|
};
|
|
?>
|
|
|
|
<div class="space-y-6">
|
|
<!-- Header Card -->
|
|
<x-shared.card class="p-6">
|
|
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
<div class="flex items-center gap-4">
|
|
<div class="relative group">
|
|
<x-mary-avatar
|
|
:image="$app->logo ? asset($app->logo) : null"
|
|
:placeholder="$app->logo ? null : 'lucide.settings'"
|
|
class="w-16 h-16 rounded-xl border border-gray-200 p-2 shadow-xs bg-gray-50 group-hover:opacity-85 transition-opacity"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<div class="flex items-center gap-2">
|
|
<h1 class="text-2xl font-bold text-gray-900 ">{{ $app->name }}</h1>
|
|
<button wire:click="openEditGeneralModal" class="text-gray-400 hover:text-gray-600 ">
|
|
<x-lucide-pencil class="w-4 h-4"/>
|
|
</button>
|
|
</div>
|
|
<p class="text-sm text-gray-500 mt-0.5">Protocol: <span
|
|
class="font-semibold text-gray-700 ">{{ strtoupper($app->protocol->name) }}</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-3">
|
|
<!-- Status Dropdown/Trigger -->
|
|
<div class="dropdown dropdown-end">
|
|
<label tabindex="0"
|
|
class="btn btn-sm btn-outline gap-1.5 font-semibold {{ $app->status->name === 'connected' ? 'border-emerald-200 text-emerald-700 bg-emerald-50/30' : ($app->status->name === 'disconnected' ? 'border-rose-200 text-rose-700 bg-rose-50/30' : 'border-gray-200 text-gray-700 bg-gray-50/30') }}">
|
|
<span
|
|
class="w-2 h-2 rounded-full {{ $app->status->name === 'connected' ? 'bg-emerald-500' : ($app->status->name === 'disconnected' ? 'bg-rose-500' : 'bg-gray-400') }}"></span>
|
|
{{ ucfirst($app->status->name) }}
|
|
<x-lucide-chevron-down class="w-3.5 h-3.5 opacity-60"/>
|
|
</label>
|
|
<ul tabindex="0"
|
|
class="dropdown-content menu p-1 shadow-md bg-base-100 rounded-box w-44 border border-gray-100 mt-1 z-30">
|
|
@foreach(App\Enums\ConnectionStatusEnum::cases() as $statusCase)
|
|
<li>
|
|
<button wire:click="changeStatus('{{ $statusCase->value }}')"
|
|
class="font-medium text-xs py-2">
|
|
<span
|
|
class="w-2 h-2 rounded-full {{ $statusCase->value === 'connected' ? 'bg-emerald-500' : ($statusCase->value === 'disconnected' ? 'bg-rose-500' : 'bg-gray-400') }}"></span>
|
|
{{ ucfirst($statusCase->value) }}
|
|
</button>
|
|
</li>
|
|
@endforeach
|
|
</ul>
|
|
</div>
|
|
|
|
<x-mary-button
|
|
icon="lucide.arrow-left"
|
|
class="btn-sm btn-ghost text-gray-500"
|
|
wire:click="goBack"
|
|
label="Back"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</x-shared.card>
|
|
|
|
<!-- Main Content Area -->
|
|
<x-shared.card class="p-0 overflow-hidden">
|
|
<x-mary-tabs wire:model="selectedTab" class="px-4" label-div-class="pt-3 bg-gray-50 border-b border-gray-200"
|
|
label-class="font-bold pb-3">
|
|
|
|
<!-- General Tab -->
|
|
<x-mary-tab name="general-tab" label="General">
|
|
<div class="space-y-6">
|
|
<x-shared.card title="Access URL">
|
|
<x-slot:actions>
|
|
@if($editAccessUrlMode)
|
|
<div class="flex gap-2">
|
|
<x-mary-button
|
|
icon="lucide.check"
|
|
class="btn-primary"
|
|
label="Save"
|
|
wire:click="saveAccessUrl"
|
|
/>
|
|
<x-mary-button
|
|
icon="lucide.x"
|
|
class="btn-ghost"
|
|
label="Cancel"
|
|
wire:click="cancelEditAccessUrl"
|
|
/>
|
|
</div>
|
|
@else
|
|
<x-mary-button
|
|
icon="lucide.pencil"
|
|
label="Edit"
|
|
wire:click="startEditAccessUrl"
|
|
class="btn-primary btn-outline"
|
|
/>
|
|
@endif
|
|
</x-slot:actions>
|
|
<div class="p-6 space-y-6">
|
|
@if($editAccessUrlMode)
|
|
<x-mary-input
|
|
type="text"
|
|
wire:model="editAccessUrl"
|
|
class="input-sm font-mono w-full bg-white border-gray-200"
|
|
placeholder="https://app.example.com"
|
|
/>
|
|
@else
|
|
<input
|
|
type="text"
|
|
readonly
|
|
value="{{data_get($app, 'settings.access_url')}}"
|
|
class="input input-sm input-bordered font-mono w-full bg-gray-50 border-gray-200 focus:outline-hidden"
|
|
/>
|
|
@endif
|
|
</div>
|
|
</x-shared.card>
|
|
</div>
|
|
</x-mary-tab>
|
|
|
|
<!-- Assignments Tab -->
|
|
<x-mary-tab name="assignments-tab" label="Assignments">
|
|
<div class="">
|
|
<x-shared.card title="Assigned Roles">
|
|
<div class="overflow-x-auto">
|
|
<table class="table w-full text-xs">
|
|
<thead>
|
|
<tr class="bg-gray-50/30 text-gray-400 border-b border-gray-100">
|
|
<th class="font-semibold py-3 px-6 text-left">Role Name</th>
|
|
<th class="font-semibold py-3 px-6 text-left">Priority</th>
|
|
<th class="font-semibold py-3 px-6 text-right">Access Type</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@forelse($app->roles as $role)
|
|
<tr class="hover:bg-gray-50/50 border-b border-gray-100">
|
|
<td class="py-4 px-6 font-semibold text-gray-800 0">
|
|
{{ $role->name }}
|
|
</td>
|
|
<td class="py-4 px-6 text-gray-500">
|
|
{{ $role->priority }}
|
|
</td>
|
|
<td class="py-4 px-6 text-right">
|
|
<span
|
|
class="badge badge-soft badge-primary font-semibold text-xs px-2.5 py-1">
|
|
SSO Access
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
@empty
|
|
<tr>
|
|
<td colspan="3" class="text-center py-6 text-gray-400 italic">
|
|
No specific roles assigned. Accessible by everyone depending on user access
|
|
setting.
|
|
</td>
|
|
</tr>
|
|
@endforelse
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</x-shared.card>
|
|
</div>
|
|
</x-mary-tab>
|
|
</x-mary-tabs>
|
|
</x-shared.card>
|
|
|
|
<!-- Edit General Info Modal -->
|
|
<x-mary-modal wire:model="editGeneralModal" title="Edit App General Settings" class="backdrop-blur">
|
|
<x-mary-form wire:submit="saveGeneralInfo">
|
|
<x-mary-input
|
|
label="Application Name"
|
|
wire:model="editName"
|
|
placeholder="e.g. My App"
|
|
required
|
|
/>
|
|
<x-mary-file
|
|
label="Application Logo"
|
|
wire:model="editLogo"
|
|
accept="image/jpeg,image/png"
|
|
hint="Optional. Max 2MB."
|
|
/>
|
|
|
|
<x-slot:actions>
|
|
<x-mary-button label="Cancel" @click="$wire.editGeneralModal = false" class="btn-ghost"/>
|
|
<x-mary-button label="Save Settings" type="submit" class="btn-primary" spinner="saveGeneralInfo"/>
|
|
</x-slot:actions>
|
|
</x-mary-form>
|
|
</x-mary-modal>
|
|
</div>
|