mirror of
https://github.com/khoaliber/dockhand.git
synced 2026-03-04 05:29:06 +00:00
442 lines
16 KiB
Svelte
442 lines
16 KiB
Svelte
<script lang="ts">
|
|
import * as Card from '$lib/components/ui/card';
|
|
import * as Select from '$lib/components/ui/select';
|
|
import { Label } from '$lib/components/ui/label';
|
|
import { Input } from '$lib/components/ui/input';
|
|
import { Button } from '$lib/components/ui/button';
|
|
import { Badge } from '$lib/components/ui/badge';
|
|
import { TogglePill, ToggleSwitch } from '$lib/components/ui/toggle-pill';
|
|
import CronEditor from '$lib/components/cron-editor.svelte';
|
|
import TimezoneSelector from '$lib/components/TimezoneSelector.svelte';
|
|
import { Eye, Bell, Database, Calendar, ShieldCheck, FileText, AlertTriangle, HelpCircle, Globe } from 'lucide-svelte';
|
|
import { appSettings, type DateFormat, type DownloadFormat } from '$lib/stores/settings';
|
|
import { canAccess, authStore } from '$lib/stores/auth';
|
|
import { toast } from 'svelte-sonner';
|
|
import ThemeSelector from '$lib/components/ThemeSelector.svelte';
|
|
import * as Tooltip from '$lib/components/ui/tooltip';
|
|
|
|
// General settings state - these derive from the store
|
|
let confirmDestructive = $derived($appSettings.confirmDestructive);
|
|
let showStoppedContainers = $derived($appSettings.showStoppedContainers);
|
|
let highlightUpdates = $derived($appSettings.highlightUpdates);
|
|
let timeFormat = $derived($appSettings.timeFormat);
|
|
let dateFormat = $derived($appSettings.dateFormat);
|
|
let downloadFormat = $derived($appSettings.downloadFormat);
|
|
let defaultGrypeArgs = $derived($appSettings.defaultGrypeArgs);
|
|
let defaultTrivyArgs = $derived($appSettings.defaultTrivyArgs);
|
|
let scheduleRetentionDays = $derived($appSettings.scheduleRetentionDays);
|
|
let eventRetentionDays = $derived($appSettings.eventRetentionDays);
|
|
let scheduleCleanupCron = $derived($appSettings.scheduleCleanupCron);
|
|
let eventCleanupCron = $derived($appSettings.eventCleanupCron);
|
|
let scheduleCleanupEnabled = $derived($appSettings.scheduleCleanupEnabled);
|
|
let eventCleanupEnabled = $derived($appSettings.eventCleanupEnabled);
|
|
let logBufferSizeKb = $derived($appSettings.logBufferSizeKb);
|
|
let defaultTimezone = $derived($appSettings.defaultTimezone);
|
|
|
|
const dateFormatOptions: { value: DateFormat; label: string; example: string }[] = [
|
|
{ value: 'DD.MM.YYYY', label: 'DD.MM.YYYY', example: '31.12.2024' },
|
|
{ value: 'DD/MM/YYYY', label: 'DD/MM/YYYY', example: '31/12/2024' },
|
|
{ value: 'MM/DD/YYYY', label: 'MM/DD/YYYY', example: '12/31/2024' },
|
|
{ value: 'YYYY-MM-DD', label: 'YYYY-MM-DD', example: '2024-12-31' }
|
|
];
|
|
|
|
function handleScheduleRetentionChange(e: Event) {
|
|
const value = Math.max(1, Math.min(365, parseInt((e.target as HTMLInputElement).value) || 30));
|
|
appSettings.setScheduleRetentionDays(value);
|
|
toast.success('Schedule retention updated');
|
|
}
|
|
|
|
function handleEventRetentionChange(e: Event) {
|
|
const value = Math.max(1, Math.min(365, parseInt((e.target as HTMLInputElement).value) || 30));
|
|
appSettings.setEventRetentionDays(value);
|
|
toast.success('Event retention updated');
|
|
}
|
|
|
|
function handleScheduleCleanupCronChange(cron: string) {
|
|
appSettings.setScheduleCleanupCron(cron);
|
|
toast.success('Schedule cleanup cron updated');
|
|
}
|
|
|
|
function handleEventCleanupCronChange(cron: string) {
|
|
appSettings.setEventCleanupCron(cron);
|
|
toast.success('Event cleanup cron updated');
|
|
}
|
|
|
|
function handleScheduleCleanupEnabledChange() {
|
|
appSettings.setScheduleCleanupEnabled(!scheduleCleanupEnabled);
|
|
toast.success(scheduleCleanupEnabled ? 'Schedule cleanup disabled' : 'Schedule cleanup enabled');
|
|
}
|
|
|
|
function handleEventCleanupEnabledChange() {
|
|
appSettings.setEventCleanupEnabled(!eventCleanupEnabled);
|
|
toast.success(eventCleanupEnabled ? 'Event cleanup disabled' : 'Event cleanup enabled');
|
|
}
|
|
|
|
function handleGrypeArgsBlur(e: Event) {
|
|
const value = (e.target as HTMLInputElement).value.trim();
|
|
if (value !== defaultGrypeArgs) {
|
|
appSettings.setDefaultGrypeArgs(value);
|
|
toast.success('Grype default arguments updated');
|
|
}
|
|
}
|
|
|
|
function handleTrivyArgsBlur(e: Event) {
|
|
const value = (e.target as HTMLInputElement).value.trim();
|
|
if (value !== defaultTrivyArgs) {
|
|
appSettings.setDefaultTrivyArgs(value);
|
|
toast.success('Trivy default arguments updated');
|
|
}
|
|
}
|
|
|
|
function handleLogBufferSizeChange(e: Event) {
|
|
const value = Math.max(100, Math.min(5000, parseInt((e.target as HTMLInputElement).value) || 500));
|
|
appSettings.setLogBufferSizeKb(value);
|
|
toast.success('Log buffer size updated');
|
|
}
|
|
</script>
|
|
|
|
<div class="flex-1 min-h-0 overflow-y-auto">
|
|
<div class="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
|
<!-- Left column -->
|
|
<div class="space-y-4">
|
|
<Card.Root>
|
|
<Card.Header>
|
|
<Card.Title class="text-sm font-medium flex items-center gap-2">
|
|
<Eye class="w-4 h-4" />
|
|
Appearance
|
|
{#if !$authStore.authEnabled}
|
|
<Tooltip.Provider delayDuration={100}>
|
|
<Tooltip.Root>
|
|
<Tooltip.Trigger>
|
|
<HelpCircle class="w-4 h-4 text-muted-foreground cursor-help" />
|
|
</Tooltip.Trigger>
|
|
<Tooltip.Portal>
|
|
<Tooltip.Content side="right" sideOffset={8} class="!w-80">
|
|
Theme and font settings are global when authentication is disabled. When auth is enabled, users can customize their appearance in their profile.
|
|
</Tooltip.Content>
|
|
</Tooltip.Portal>
|
|
</Tooltip.Root>
|
|
</Tooltip.Provider>
|
|
{/if}
|
|
</Card.Title>
|
|
</Card.Header>
|
|
<Card.Content>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4">
|
|
<!-- Left column -->
|
|
<div class="space-y-4">
|
|
<div class="space-y-1">
|
|
<div class="flex items-center gap-3">
|
|
<Label>Show stopped containers</Label>
|
|
<TogglePill
|
|
checked={showStoppedContainers}
|
|
onchange={() => {
|
|
appSettings.setShowStoppedContainers(!showStoppedContainers);
|
|
toast.success(showStoppedContainers ? 'Stopped containers hidden' : 'Stopped containers shown');
|
|
}}
|
|
disabled={!$canAccess('settings', 'edit')}
|
|
/>
|
|
</div>
|
|
<p class="text-xs text-muted-foreground">Display stopped and exited containers in lists</p>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<div class="flex items-center gap-3">
|
|
<Label>Highlight available updates</Label>
|
|
<TogglePill
|
|
checked={highlightUpdates}
|
|
onchange={() => {
|
|
appSettings.setHighlightUpdates(!highlightUpdates);
|
|
toast.success(highlightUpdates ? 'Update highlighting disabled' : 'Update highlighting enabled');
|
|
}}
|
|
disabled={!$canAccess('settings', 'edit')}
|
|
/>
|
|
</div>
|
|
<p class="text-xs text-muted-foreground">Highlight container rows in amber when updates are available</p>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<div class="flex items-center gap-3">
|
|
<Label>Time format</Label>
|
|
<ToggleSwitch
|
|
value={timeFormat}
|
|
leftValue="24h"
|
|
rightValue="12h"
|
|
onchange={(newFormat) => {
|
|
appSettings.setTimeFormat(newFormat as '12h' | '24h');
|
|
toast.success(`Time format set to ${newFormat === '12h' ? '12-hour (AM/PM)' : '24-hour'}`);
|
|
}}
|
|
disabled={!$canAccess('settings', 'edit')}
|
|
/>
|
|
</div>
|
|
<p class="text-xs text-muted-foreground">Display timestamps in 12-hour (AM/PM) or 24-hour format</p>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<div class="flex items-center gap-3">
|
|
<Label>Date format</Label>
|
|
<Select.Root
|
|
type="single"
|
|
value={dateFormat}
|
|
onValueChange={(value) => {
|
|
if (value) {
|
|
appSettings.setDateFormat(value as DateFormat);
|
|
toast.success(`Date format set to ${value}`);
|
|
}
|
|
}}
|
|
disabled={!$canAccess('settings', 'edit')}
|
|
>
|
|
<Select.Trigger class="w-[180px]">
|
|
<Calendar class="w-4 h-4 mr-2" />
|
|
<span>{dateFormat}</span>
|
|
</Select.Trigger>
|
|
<Select.Content>
|
|
{#each dateFormatOptions as option}
|
|
<Select.Item value={option.value}>
|
|
<div class="flex items-center justify-between w-full gap-4">
|
|
<span>{option.label}</span>
|
|
<span class="text-xs text-muted-foreground">{option.example}</span>
|
|
</div>
|
|
</Select.Item>
|
|
{/each}
|
|
</Select.Content>
|
|
</Select.Root>
|
|
</div>
|
|
<p class="text-xs text-muted-foreground">How dates are displayed throughout the app</p>
|
|
</div>
|
|
</div>
|
|
<!-- Right column: Theme settings (only when auth disabled) -->
|
|
{#if !$authStore.authEnabled}
|
|
<div class="space-y-4">
|
|
<ThemeSelector />
|
|
</div>
|
|
{:else}
|
|
<div class="text-xs text-muted-foreground flex items-start gap-1.5">
|
|
<HelpCircle class="w-3.5 h-3.5 shrink-0 mt-0.5" />
|
|
<div>
|
|
<p>Appearance settings (theme, fonts) are personal when auth is enabled.</p>
|
|
<a href="/profile" class="text-primary hover:underline">Configure in your profile</a>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</Card.Content>
|
|
</Card.Root>
|
|
|
|
<Card.Root>
|
|
<Card.Header>
|
|
<Card.Title class="text-sm font-medium flex items-center gap-2">
|
|
<Globe class="w-4 h-4" />
|
|
Scheduling
|
|
</Card.Title>
|
|
</Card.Header>
|
|
<Card.Content class="space-y-4">
|
|
<div class="space-y-2">
|
|
<Label>Default timezone</Label>
|
|
<TimezoneSelector
|
|
value={defaultTimezone}
|
|
onchange={(value) => {
|
|
appSettings.setDefaultTimezone(value);
|
|
toast.success(`Default timezone set to ${value}`);
|
|
}}
|
|
class="w-[320px]"
|
|
/>
|
|
<p class="text-xs text-muted-foreground">Default timezone for new environments. Used for scheduled tasks like auto-updates.</p>
|
|
</div>
|
|
</Card.Content>
|
|
</Card.Root>
|
|
|
|
<Card.Root>
|
|
<Card.Header>
|
|
<Card.Title class="text-sm font-medium flex items-center gap-2">
|
|
<Bell class="w-4 h-4" />
|
|
Confirmations
|
|
</Card.Title>
|
|
</Card.Header>
|
|
<Card.Content class="space-y-4">
|
|
<div class="space-y-1">
|
|
<div class="flex items-center gap-3">
|
|
<Label>Confirm destructive actions</Label>
|
|
<TogglePill
|
|
checked={confirmDestructive}
|
|
onchange={() => {
|
|
appSettings.setConfirmDestructive(!confirmDestructive);
|
|
toast.success(confirmDestructive ? 'Confirmations disabled' : 'Confirmations enabled');
|
|
}}
|
|
disabled={!$canAccess('settings', 'edit')}
|
|
/>
|
|
</div>
|
|
<p class="text-xs text-muted-foreground">Show confirmation dialogs before deleting resources</p>
|
|
</div>
|
|
</Card.Content>
|
|
</Card.Root>
|
|
|
|
<Card.Root>
|
|
<Card.Header>
|
|
<Card.Title class="text-sm font-medium flex items-center gap-2">
|
|
<FileText class="w-4 h-4" />
|
|
Logs & files
|
|
</Card.Title>
|
|
</Card.Header>
|
|
<Card.Content class="space-y-4">
|
|
<div class="space-y-2">
|
|
<Label for="log-buffer-size">Log buffer size (KB)</Label>
|
|
<div class="flex items-center gap-2">
|
|
<Input
|
|
id="log-buffer-size"
|
|
type="number"
|
|
min="100"
|
|
max="5000"
|
|
value={logBufferSizeKb}
|
|
onchange={handleLogBufferSizeChange}
|
|
disabled={!$canAccess('settings', 'edit')}
|
|
class="w-24"
|
|
/>
|
|
<span class="text-sm text-muted-foreground">KB</span>
|
|
</div>
|
|
<p class="text-xs text-muted-foreground">Maximum log buffer per container panel. Older logs are truncated when exceeded.</p>
|
|
{#if logBufferSizeKb > 1000}
|
|
<div class="flex items-start gap-2 p-2 rounded-md bg-amber-500/10 border border-amber-500/20">
|
|
<AlertTriangle class="w-4 h-4 text-amber-500 shrink-0 mt-0.5" />
|
|
<p class="text-xs text-amber-600 dark:text-amber-400">High values may degrade browser performance with verbose containers. Recommended: 250-1000 KB.</p>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
<div class="space-y-1">
|
|
<div class="flex items-center gap-3">
|
|
<Label>Download format</Label>
|
|
<ToggleSwitch
|
|
value={downloadFormat}
|
|
leftValue="tar"
|
|
rightValue="tar.gz"
|
|
onchange={(newFormat) => {
|
|
appSettings.setDownloadFormat(newFormat as DownloadFormat);
|
|
toast.success(`Download format set to ${newFormat}`);
|
|
}}
|
|
disabled={!$canAccess('settings', 'edit')}
|
|
/>
|
|
</div>
|
|
<p class="text-xs text-muted-foreground">Archive format when downloading files from containers</p>
|
|
</div>
|
|
</Card.Content>
|
|
</Card.Root>
|
|
|
|
</div>
|
|
|
|
<!-- Right column -->
|
|
<div class="space-y-4">
|
|
<Card.Root>
|
|
<Card.Header>
|
|
<Card.Title class="text-sm font-medium flex items-center gap-2">
|
|
<ShieldCheck class="w-4 h-4" />
|
|
Vulnerability scanners
|
|
</Card.Title>
|
|
</Card.Header>
|
|
<Card.Content class="space-y-4">
|
|
<div class="space-y-2">
|
|
<Label for="grype-args">Default Grype arguments</Label>
|
|
<Input
|
|
id="grype-args"
|
|
value={defaultGrypeArgs}
|
|
onblur={handleGrypeArgsBlur}
|
|
disabled={!$canAccess('settings', 'edit')}
|
|
placeholder={"-o json -v {image}"}
|
|
/>
|
|
<p class="text-xs text-muted-foreground">Use <code class="bg-muted px-1 rounded">{'{image}'}</code> as placeholder for the image name</p>
|
|
</div>
|
|
<div class="space-y-2">
|
|
<Label for="trivy-args">Default Trivy arguments</Label>
|
|
<Input
|
|
id="trivy-args"
|
|
value={defaultTrivyArgs}
|
|
onblur={handleTrivyArgsBlur}
|
|
disabled={!$canAccess('settings', 'edit')}
|
|
placeholder={"image --format json {image}"}
|
|
/>
|
|
<p class="text-xs text-muted-foreground">Use <code class="bg-muted px-1 rounded">{'{image}'}</code> as placeholder for the image name</p>
|
|
</div>
|
|
</Card.Content>
|
|
</Card.Root>
|
|
|
|
<Card.Root>
|
|
<Card.Header>
|
|
<Card.Title class="text-sm font-medium flex items-center gap-2">
|
|
<Database class="w-4 h-4" />
|
|
System jobs
|
|
</Card.Title>
|
|
</Card.Header>
|
|
<Card.Content class="space-y-4">
|
|
<div class="space-y-1">
|
|
<div class="flex items-center gap-3">
|
|
<Label for="schedule-retention">Schedule execution cleanup</Label>
|
|
<TogglePill
|
|
checked={scheduleCleanupEnabled}
|
|
onchange={handleScheduleCleanupEnabledChange}
|
|
disabled={!$canAccess('settings', 'edit')}
|
|
/>
|
|
</div>
|
|
<p class="text-xs text-muted-foreground">Delete executions older than specified days</p>
|
|
<div class="flex items-center gap-2 mt-2">
|
|
<Input
|
|
id="schedule-retention"
|
|
type="number"
|
|
min="1"
|
|
max="365"
|
|
value={scheduleRetentionDays}
|
|
onchange={handleScheduleRetentionChange}
|
|
disabled={!$canAccess('settings', 'edit') || !scheduleCleanupEnabled}
|
|
class="w-20"
|
|
/>
|
|
<span class="text-sm text-muted-foreground">days</span>
|
|
<div class="ml-auto">
|
|
<CronEditor
|
|
value={scheduleCleanupCron}
|
|
onchange={handleScheduleCleanupCronChange}
|
|
disabled={!$canAccess('settings', 'edit') || !scheduleCleanupEnabled}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<div class="flex items-center gap-3">
|
|
<Label for="event-retention">Container event cleanup</Label>
|
|
<TogglePill
|
|
checked={eventCleanupEnabled}
|
|
onchange={handleEventCleanupEnabledChange}
|
|
disabled={!$canAccess('settings', 'edit')}
|
|
/>
|
|
</div>
|
|
<p class="text-xs text-muted-foreground">Delete events older than specified days</p>
|
|
<div class="flex items-center gap-2 mt-2">
|
|
<Input
|
|
id="event-retention"
|
|
type="number"
|
|
min="1"
|
|
max="365"
|
|
value={eventRetentionDays}
|
|
onchange={handleEventRetentionChange}
|
|
disabled={!$canAccess('settings', 'edit') || !eventCleanupEnabled}
|
|
class="w-20"
|
|
/>
|
|
<span class="text-sm text-muted-foreground">days</span>
|
|
<div class="ml-auto">
|
|
<CronEditor
|
|
value={eventCleanupCron}
|
|
onchange={handleEventCleanupCronChange}
|
|
disabled={!$canAccess('settings', 'edit') || !eventCleanupEnabled}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="space-y-1 pt-2 border-t">
|
|
<div class="flex items-center gap-3">
|
|
<Label>Volume helper cleanup</Label>
|
|
<Badge variant="secondary" class="text-xs">Always enabled</Badge>
|
|
</div>
|
|
<p class="text-xs text-muted-foreground">
|
|
Automatically removes temporary containers used for browsing volume contents.
|
|
Runs every 30 minutes and on startup.
|
|
</p>
|
|
</div>
|
|
</Card.Content>
|
|
</Card.Root>
|
|
</div>
|
|
</div>
|
|
</div>
|