feat(volume-backups): add service backup UI and polish layouts

Add dedicated service volume-backup Livewire pages and routes, wire
shared storage backups for services, and pass S3 trusted hosts into
MinIO resolve options. Also refine breadcrumbs, sticky sidebars,
destination/GitHub layouts, resource filters, and login recovery link.
This commit is contained in:
Andras Bacsai committed 2026-08-05 15:15:50 +02:00
1 parent 0312cc8ffb
commit 5842d77b81
69 files changed
+1607 -501

No files matched your search

@@ -6,7 +6,7 @@
@if (data_get($github_app, 'app_id'))
@php
$githubAppRouteParameters = ['github_app_uuid' => $github_app->uuid];
$showSettingsSidebar = in_array($activeTab, ['general', 'danger'], true);
$showSettingsSidebar = in_array($activeTab, ['general', 'permissions', 'resources', 'danger'], true);
$settingsMenuItems = [
[
'label' => 'General',
@@ -14,6 +14,18 @@
'active' => $activeTab === 'general',
'icon' => 'settings',
],
[
'label' => 'Permissions',
'route' => 'source.github.permissions',
'active' => $activeTab === 'permissions',
'icon' => 'keys',
],
[
'label' => 'Resources',
'route' => 'source.github.resources',
'active' => $activeTab === 'resources',
'icon' => 'grid',
],
[
'label' => 'Danger Zone',
'route' => 'source.github.danger',
@@ -26,20 +38,12 @@
<x-dashboard.navbar section="source" :parameters="$githubAppRouteParameters"
:title="$name ?: 'GitHub App'"
:subtitle="filled($organization) ? 'GitHub App for '.$organization : 'Private GitHub source'"
:titleOnDesktop="true">
<x-slot:titleMeta>
@if (data_get($github_app, 'installation_id'))
<x-status-badge label="Connected" type="success" />
@else
<x-status-badge label="Setup incomplete" type="warning" />
@endif
</x-slot:titleMeta>
</x-dashboard.navbar>
:mobileTitleOnly="true" />
@if ($showSettingsSidebar)
<section class="application-settings-workspace mt-4 w-full max-w-[1180px] lg:mt-0">
<div class="grid min-w-0 gap-8 xl:grid-cols-[210px_minmax(0,1fr)] xl:gap-10">
<aside class="application-settings-navigation min-w-0 xl:sticky xl:top-26 xl:self-start">
<aside class="application-settings-navigation min-w-0 xl:self-start">
<nav aria-label="GitHub App settings"
class="grid grid-cols-2 gap-0.5 border-y border-neutral-200 py-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-1 xl:border-y-0 xl:py-0 dark:border-white/[0.06]">
<div class="nav-section hidden xl:block">Settings</div>
@@ -58,7 +62,7 @@
</nav>
</aside>
<div class="min-w-0 xl:mt-3">
<div class="min-w-0">
@if (!data_get($github_app, 'installation_id') && $activeTab === 'general')
<div class="application-settings-form">
<x-application.settings-section title="Complete GitHub installation"
@@ -213,87 +217,14 @@
@endcannot
</x-application.settings-section>
</div>
@elseif ($activeTab === 'permissions')
@include('livewire.source.github.permissions')
@elseif ($activeTab === 'resources')
@include('livewire.source.github.resources')
@endif
</div>
</div>
</section>
@elseif ($activeTab === 'permissions')
<div class="application-settings-form">
<x-application.settings-section title="Permissions"
description="GitHub permissions currently granted to this App.">
<x-slot:actions>
@can('view', $github_app)
<x-forms.button type="button" wire:click.prevent="checkPermissions">
<x-reicon name="refresh" class="size-3.5" />
Refetch
</x-forms.button>
<a href="{{ getPermissionsPath($github_app) }}" class="button">
Update on GitHub
<x-external-link />
</a>
@endcan
</x-slot:actions>
<div class="grid gap-4 lg:grid-cols-3">
<x-forms.input canGate="view" :canResource="$github_app" id="contents"
helper="Read access is mandatory." label="Contents" readonly placeholder="N/A" />
<x-forms.input canGate="view" :canResource="$github_app" id="metadata"
helper="Read access is mandatory." label="Metadata" readonly placeholder="N/A" />
<x-forms.input canGate="view" :canResource="$github_app" id="pullRequests"
helper="Write access is needed for preview deployment status updates."
label="Pull requests" readonly placeholder="N/A" />
</div>
</x-application.settings-section>
</div>
@else
<div x-data="{ search: '' }" class="application-settings-form">
<x-application.settings-section title="Resources"
description="Applications currently using this GitHub App." flush>
@if ($applications->isEmpty())
<x-empty title="No resources use this source"
description="Applications will appear here after this GitHub App is selected as their source."
icon-name="sources" size="sm" />
@else
<div class="border-b border-neutral-200 p-3 dark:border-white/[0.08]">
<div class="relative w-full max-w-sm">
<x-reicon name="search"
class="pointer-events-none absolute top-1/2 left-2.5 z-10 size-3.5 -translate-y-1/2 text-neutral-400 dark:text-fg-faint" />
<input x-model.debounce.150ms="search" type="search"
placeholder="Search resources"
class="h-8! w-full rounded-lg! border-neutral-200! bg-white! py-0! pr-3! pl-8! text-[12px]! shadow-none! placeholder:text-neutral-400 focus:border-accent! focus:ring-0! dark:border-white/[0.08]! dark:bg-white/[0.035]! dark:text-fg! dark:placeholder:text-fg-faint">
</div>
</div>
<div class="overflow-x-auto">
<div
class="grid min-w-[680px] grid-cols-[minmax(10rem,.8fr)_minmax(10rem,.8fr)_minmax(12rem,1fr)_8rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
<div>Project</div>
<div>Environment</div>
<div>Resource</div>
<div>Type</div>
</div>
@foreach ($applications->sortBy('name', SORT_NATURAL) as $resource)
@php
$projectName = (string) data_get($resource->project(), 'name');
$environmentName = (string) data_get($resource, 'environment.name');
$resourceName = (string) $resource->name;
$resourceType = (string) str($resource->type())->headline();
$searchValue = strtolower(
$projectName.' '.$environmentName.' '.$resourceName.' '.$resourceType,
);
@endphp
<a {{ wireNavigate() }} href="{{ $resource->link() }}"
x-show="search === '' || '{{ addslashes($searchValue) }}'.includes(search.toLowerCase())"
class="grid min-h-13 min-w-[680px] grid-cols-[minmax(10rem,.8fr)_minmax(10rem,.8fr)_minmax(12rem,1fr)_8rem] items-center border-b border-neutral-200 px-4 py-2.5 text-[12px] transition-colors last:border-b-0 hover:bg-neutral-50 hover:no-underline dark:border-white/[0.07] dark:hover:bg-white/[0.025]">
<span class="truncate text-neutral-500 dark:text-fg-dim">{{ $projectName }}</span>
<span class="truncate text-neutral-500 dark:text-fg-dim">{{ $environmentName }}</span>
<span class="truncate font-medium text-black dark:text-fg">{{ $resourceName }}</span>
<span class="text-neutral-500 dark:text-fg-dim">{{ $resourceType }}</span>
</a>
@endforeach
</div>
@endif
</x-application.settings-section>
</div>
@endif
@else
<header class="mb-8 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
@@ -0,0 +1,27 @@
<div class="application-settings-form">
<x-application.settings-section title="Permissions"
description="GitHub permissions currently granted to this App.">
<x-slot:actions>
@can('view', $github_app)
<x-forms.button type="button" wire:click.prevent="checkPermissions">
<x-reicon name="refresh" class="size-3.5" />
Refetch
</x-forms.button>
<a href="{{ getPermissionsPath($github_app) }}" class="button">
Update on GitHub
<x-external-link />
</a>
@endcan
</x-slot:actions>
<div class="grid gap-4 lg:grid-cols-3">
<x-forms.input canGate="view" :canResource="$github_app" id="contents"
helper="Read access is mandatory." label="Contents" readonly placeholder="N/A" />
<x-forms.input canGate="view" :canResource="$github_app" id="metadata"
helper="Read access is mandatory." label="Metadata" readonly placeholder="N/A" />
<x-forms.input canGate="view" :canResource="$github_app" id="pullRequests"
helper="Write access is needed for preview deployment status updates."
label="Pull requests" readonly placeholder="N/A" />
</div>
</x-application.settings-section>
</div>
@@ -0,0 +1,48 @@
<div x-data="{ search: '' }" class="application-settings-form">
<x-application.settings-section title="Resources"
description="Applications currently using this GitHub App." flush>
@if ($applications->isEmpty())
<x-empty title="No resources use this source"
description="Applications will appear here after this GitHub App is selected as their source."
icon-name="sources" size="sm" />
@else
<div class="border-b border-neutral-200 p-3 dark:border-white/[0.08]">
<div class="relative w-full max-w-sm">
<x-reicon name="search"
class="pointer-events-none absolute top-1/2 left-2.5 z-10 size-3.5 -translate-y-1/2 text-neutral-400 dark:text-fg-faint" />
<input x-model.debounce.150ms="search" type="search"
placeholder="Search resources"
class="h-8! w-full rounded-lg! border-neutral-200! bg-white! py-0! pr-3! pl-8! text-[12px]! shadow-none! placeholder:text-neutral-400 focus:border-accent! focus:ring-0! dark:border-white/[0.08]! dark:bg-white/[0.035]! dark:text-fg! dark:placeholder:text-fg-faint">
</div>
</div>
<div class="overflow-x-auto">
<div
class="grid min-w-[680px] grid-cols-[minmax(10rem,.8fr)_minmax(10rem,.8fr)_minmax(12rem,1fr)_8rem] border-b border-neutral-200 bg-neutral-50 px-4 py-2.5 text-[11px] font-medium text-neutral-500 dark:border-white/[0.08] dark:bg-white/[0.025] dark:text-fg-faint">
<div>Project</div>
<div>Environment</div>
<div>Resource</div>
<div>Type</div>
</div>
@foreach ($applications->sortBy('name', SORT_NATURAL) as $resource)
@php
$projectName = (string) data_get($resource->project(), 'name');
$environmentName = (string) data_get($resource, 'environment.name');
$resourceName = (string) $resource->name;
$resourceType = (string) str($resource->type())->headline();
$searchValue = strtolower(
$projectName.' '.$environmentName.' '.$resourceName.' '.$resourceType,
);
@endphp
<a {{ wireNavigate() }} href="{{ $resource->link() }}"
x-show="search === '' || '{{ addslashes($searchValue) }}'.includes(search.toLowerCase())"
class="grid min-h-13 min-w-[680px] grid-cols-[minmax(10rem,.8fr)_minmax(10rem,.8fr)_minmax(12rem,1fr)_8rem] items-center border-b border-neutral-200 px-4 py-2.5 text-[12px] transition-colors last:border-b-0 hover:bg-neutral-50 hover:no-underline dark:border-white/[0.07] dark:hover:bg-white/[0.025]">
<span class="truncate text-neutral-500 dark:text-fg-dim">{{ $projectName }}</span>
<span class="truncate text-neutral-500 dark:text-fg-dim">{{ $environmentName }}</span>
<span class="truncate font-medium text-black dark:text-fg">{{ $resourceName }}</span>
<span class="text-neutral-500 dark:text-fg-dim">{{ $resourceType }}</span>
</a>
@endforeach
</div>
@endif
</x-application.settings-section>
</div>