Design tokens - Fix WCAG AA text failures: darken light muted text and lighten dark tertiary text so 13-14px copy clears 4.5:1 on every surface. - Rebuild the dark surface ladder in correct values (oklch compresses to near-black below ~15%), giving visible steps: content, chrome, cards. - Establish a 3-layer shell (deep content, lighter sidebar/topbar chrome, lifted cards) with crisp hairline rings and a restrained card shadow. - Unify every surface to one neutral temperature (pure gray); neutralize the cool-tinted sidebar text and legacy surface tokens. - Convert all color tokens to oklch (neutrals + brand). - Unify inputs to the recessed token; fix invisible dark placeholders. - One shared --shadow-dropdown for all menus/listboxes/command palette. Settings sidebar - Sticky contained-card rail with subtle scrollbar; collapsible sub-section groups (active open by default), cross-page section links. - Mobile: collapsible disclosure with animated open, outside/Escape dismiss, press feedback, and reduced-motion support. Mobile - Consolidate the resource header (title + status + links on one row). - Tighten settings-page vertical rhythm. - Rebuild the main sidebar as an animated shadcn-style sheet (slide/fade, Escape, scroll lock, in-panel close, close-on-navigate). Fixes - #11532: selected server invisible in the light-mode terminal toolbar. Docs: update DESIGN.md tokens, shell layering, and temperature rules.
127 lines
7 KiB
PHP
127 lines
7 KiB
PHP
<div class="flex min-w-0 flex-col gap-3" x-data="{
|
|
search: '',
|
|
backups: @js($database->scheduledBackups->map(fn ($backup) => [
|
|
'name' => strtolower($database->name),
|
|
'frequency' => strtolower($backup->frequency),
|
|
's3_storage' => strtolower($backup->s3?->name ?? ''),
|
|
])->values()),
|
|
hasMatches() {
|
|
const query = this.search.toLowerCase();
|
|
|
|
return this.backups.some((backup) =>
|
|
backup.name.includes(query)
|
|
|| backup.frequency.includes(query)
|
|
|| backup.s3_storage.includes(query)
|
|
);
|
|
},
|
|
matchCount() {
|
|
const query = this.search.toLowerCase();
|
|
|
|
return this.backups.filter((backup) =>
|
|
backup.name.includes(query)
|
|
|| backup.frequency.includes(query)
|
|
|| backup.s3_storage.includes(query)
|
|
).length;
|
|
},
|
|
}">
|
|
@if ($database->is_migrated && blank($database->custom_type))
|
|
<form wire:submit="setCustomType" class="grid gap-4 p-5 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-end">
|
|
<div>
|
|
<x-forms.listbox id="custom_type" label="Database type" :options="[
|
|
['value' => 'mysql', 'label' => 'MySQL'],
|
|
['value' => 'mariadb', 'label' => 'MariaDB'],
|
|
['value' => 'postgresql', 'label' => 'PostgreSQL'],
|
|
['value' => 'mongodb', 'label' => 'MongoDB'],
|
|
]" />
|
|
<p class="mt-2 text-xs text-neutral-500 dark:text-fg-dim">
|
|
Select the database engine before enabling automated backups.
|
|
</p>
|
|
</div>
|
|
<x-forms.button type="submit">Set database type</x-forms.button>
|
|
</form>
|
|
@else
|
|
@if ($database->scheduledBackups->isNotEmpty())
|
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
<div class="relative 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 type="search" x-model="search" aria-label="Search backups"
|
|
class="input h-8! w-full pl-8! text-[13px]!" placeholder="Search backups" />
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
@if ($database->scheduledBackups->isEmpty())
|
|
<x-empty size="sm" title="No scheduled backups"
|
|
description="Create a schedule to start protecting this database."
|
|
icon-name="storages" />
|
|
@else
|
|
<div x-cloak x-show="search === '' || hasMatches()" class="data-table overflow-hidden rounded-xl border border-neutral-200 bg-white shadow-sm dark:border-white/[0.08] dark:bg-white/[0.05]">
|
|
<div class="data-table-header scheduled-backups-table-grid">
|
|
<span>Schedule</span>
|
|
<span>Latest run</span>
|
|
<span>S3 storage</span>
|
|
<span>Executions</span>
|
|
<span class="text-right">Action</span>
|
|
</div>
|
|
@foreach ($database->scheduledBackups as $backup)
|
|
@php
|
|
$latestStatus = data_get($backup->latest_log, 'status');
|
|
[$statusLabel, $statusType] = match ($latestStatus) {
|
|
'success' => ['Success', 'success'],
|
|
'running' => ['In progress', 'warning'],
|
|
'failed' => ['Failed', 'error'],
|
|
default => ['Never run', 'neutral'],
|
|
};
|
|
$backupRoute = $type === 'database'
|
|
? route('project.database.backup.execution', [...$parameters, 'backup_uuid' => $backup->uuid])
|
|
: route('project.service.database.backup.show', [...$parameters, 'backup_uuid' => $backup->uuid]);
|
|
$backupExecutionsRoute = $type === 'database'
|
|
? route('project.database.backup.executions', [...$parameters, 'backup_uuid' => $backup->uuid])
|
|
: route('project.service.database.backup.executions', [...$parameters, 'backup_uuid' => $backup->uuid]);
|
|
@endphp
|
|
<div x-show="search === ''
|
|
|| @js(strtolower($database->name)).includes(search.toLowerCase())
|
|
|| @js(strtolower($backup->frequency)).includes(search.toLowerCase())
|
|
|| @js(strtolower($backup->s3?->name ?? '')).includes(search.toLowerCase())"
|
|
class="data-table-row scheduled-backups-table-grid border-b border-neutral-200 last:border-b-0 dark:border-white/[0.06]">
|
|
<div class="min-w-0">
|
|
<span class="block truncate text-[12px] font-semibold text-black dark:text-fg">
|
|
{{ $backup->frequency }}
|
|
</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<x-status-badge :status="$statusLabel" :type="$statusType" />
|
|
@if ($latestStatus === 'running')
|
|
<x-loading />
|
|
@endif
|
|
</div>
|
|
<div class="truncate text-[11px] text-neutral-600 dark:text-fg-dim">
|
|
{{ $backup->save_s3 ? ($backup->s3?->name ?? 'Unavailable') : 'Local only' }}
|
|
</div>
|
|
<div class="text-[11px] text-neutral-600 dark:text-fg-dim">
|
|
<a wire:navigate href="{{ $backupExecutionsRoute }}"
|
|
class="font-medium hover:underline hover:text-black dark:hover:text-fg">
|
|
{{ $backup->executions_count ?? $backup->executions()->count() }}
|
|
</a>
|
|
</div>
|
|
<div class="flex justify-end">
|
|
<a class="button" wire:navigate href="{{ $backupRoute }}">Manage</a>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
<div
|
|
class="flex min-h-11 items-center border-t border-neutral-200 px-4 text-[11px] text-neutral-500 dark:border-white/[0.08] dark:text-fg-faint">
|
|
<span
|
|
x-text="`${search === '' ? backups.length : matchCount()} ${(search === '' ? backups.length : matchCount()) === 1 ? 'schedule' : 'schedules'}`"></span>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<div x-cloak x-show="search !== '' && backups.length > 0 && !hasMatches()"
|
|
class="border-t border-neutral-200 dark:border-white/[0.06]">
|
|
<x-empty size="sm" title="No matching backup schedules"
|
|
description="Try another database name, frequency, or storage name." />
|
|
</div>
|
|
@endif
|
|
</div>
|