Files
dockhand/routes/settings/general/GeneralTab.svelte
Jarek Krochmalski 62e3c6439e Initial commit
2025-12-28 21:16:03 +01:00

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>