mirror of
https://github.com/Finsys/dockhand.git
synced 2026-06-17 19:09:33 +03:00
690 lines
26 KiB
Svelte
690 lines
26 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, Activity, Clock, Info } from 'lucide-svelte';
|
|
import { appSettings, type DateFormat, type DownloadFormat, type EventCollectionMode } 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 compactPorts = $derived($appSettings.compactPorts);
|
|
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 defaultGrypeImage = $derived($appSettings.defaultGrypeImage);
|
|
let defaultTrivyImage = $derived($appSettings.defaultTrivyImage);
|
|
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 formatLogTimestamps = $derived($appSettings.formatLogTimestamps);
|
|
let defaultTimezone = $derived($appSettings.defaultTimezone);
|
|
let eventCollectionMode = $derived($appSettings.eventCollectionMode);
|
|
let eventPollInterval = $derived($appSettings.eventPollInterval);
|
|
let metricsCollectionInterval = $derived($appSettings.metricsCollectionInterval);
|
|
|
|
let clearingCache = $state(false);
|
|
|
|
async function clearScannerCache() {
|
|
clearingCache = true;
|
|
try {
|
|
const res = await fetch('/api/settings/scanner/cache', { method: 'DELETE' });
|
|
const data = await res.json();
|
|
if (res.ok && data.success) {
|
|
const total = (data.removedVolumes?.length || 0) + (data.removedDirs?.length || 0);
|
|
if (total > 0) {
|
|
toast.success(`Scanner cache cleared (${total} items removed)`);
|
|
} else {
|
|
toast.info('Scanner cache was already empty');
|
|
}
|
|
} else {
|
|
toast.error(data.error || 'Failed to clear scanner cache');
|
|
}
|
|
} catch {
|
|
toast.error('Failed to clear scanner cache');
|
|
} finally {
|
|
clearingCache = false;
|
|
}
|
|
}
|
|
|
|
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 handleGrypeImageBlur(e: Event) {
|
|
const value = (e.target as HTMLInputElement).value.trim();
|
|
if (value && value !== defaultGrypeImage) {
|
|
appSettings.setDefaultGrypeImage(value);
|
|
toast.success('Grype image updated');
|
|
}
|
|
}
|
|
|
|
function handleTrivyImageBlur(e: Event) {
|
|
const value = (e.target as HTMLInputElement).value.trim();
|
|
if (value && value !== defaultTrivyImage) {
|
|
appSettings.setDefaultTrivyImage(value);
|
|
toast.success('Trivy image updated');
|
|
}
|
|
}
|
|
|
|
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');
|
|
}
|
|
|
|
function handleEventCollectionModeChange(value: string | undefined) {
|
|
if (value === 'stream' || value === 'poll') {
|
|
appSettings.setEventCollectionMode(value);
|
|
toast.success(`Event collection mode: ${value}`);
|
|
}
|
|
}
|
|
|
|
function handleEventPollIntervalChange(selected: { value: number } | undefined) {
|
|
if (selected?.value) {
|
|
appSettings.setEventPollInterval(selected.value);
|
|
toast.success(`Event poll interval: ${selected.value / 1000}s`);
|
|
}
|
|
}
|
|
|
|
function handleMetricsIntervalChange(selected: { value: number } | undefined) {
|
|
if (selected?.value) {
|
|
appSettings.setMetricsCollectionInterval(selected.value);
|
|
toast.success(`Metrics interval: ${selected.value / 1000}s`);
|
|
}
|
|
}
|
|
</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
|
|
<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">
|
|
{#if $authStore.authEnabled}
|
|
These settings apply to the login page and as defaults. Personal preferences can be configured in your profile.
|
|
{:else}
|
|
Theme and font settings are global when authentication is disabled.
|
|
{/if}
|
|
</Tooltip.Content>
|
|
</Tooltip.Portal>
|
|
</Tooltip.Root>
|
|
</Tooltip.Provider>
|
|
</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>Compact port display</Label>
|
|
<TogglePill
|
|
checked={compactPorts}
|
|
onchange={() => {
|
|
appSettings.setCompactPorts(!compactPorts);
|
|
toast.success(compactPorts ? 'Showing all ports' : 'Compact port display enabled');
|
|
}}
|
|
disabled={!$canAccess('settings', 'edit')}
|
|
/>
|
|
</div>
|
|
<p class="text-xs text-muted-foreground">Show first port with +N count instead of all ports</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 (always shown, with hint when auth enabled) -->
|
|
<div class="space-y-4">
|
|
<ThemeSelector />
|
|
{#if $authStore.authEnabled}
|
|
<div class="text-xs text-muted-foreground flex items-start gap-1.5 mt-2 p-2 bg-muted/50 rounded-md">
|
|
<HelpCircle class="w-3.5 h-3.5 shrink-0 mt-0.5" />
|
|
<div>
|
|
<p>Personal theme preferences can be configured in your <a href="/profile" class="text-primary hover:underline">profile</a>.</p>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</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>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4">
|
|
<div 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>
|
|
</div>
|
|
<div class="space-y-4">
|
|
<div class="space-y-1">
|
|
<div class="flex items-center gap-3">
|
|
<Label>Format log timestamps</Label>
|
|
<TogglePill
|
|
checked={formatLogTimestamps}
|
|
onchange={() => {
|
|
appSettings.setFormatLogTimestamps(!formatLogTimestamps);
|
|
toast.success(formatLogTimestamps ? 'Log timestamp formatting disabled' : 'Log timestamp formatting enabled');
|
|
}}
|
|
disabled={!$canAccess('settings', 'edit')}
|
|
/>
|
|
</div>
|
|
<p class="text-xs text-muted-foreground">Convert ISO timestamps in logs to your configured date/time format</p>
|
|
<div class="flex items-start gap-1.5 mt-1">
|
|
<Info class="w-3.5 h-3.5 text-muted-foreground shrink-0 mt-0.5" />
|
|
<p class="text-xs text-muted-foreground">Docker logs use UTC timestamps by default. When enabled, timestamps like <code class="bg-muted px-1 rounded">2026-01-12T07:47:44Z</code> are converted to local time using your date/time settings.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</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-image">Grype image</Label>
|
|
<Input
|
|
id="grype-image"
|
|
value={defaultGrypeImage}
|
|
onblur={handleGrypeImageBlur}
|
|
disabled={!$canAccess('settings', 'edit')}
|
|
placeholder={"anchore/grype:v0.110.0"}
|
|
/>
|
|
<p class="text-xs text-muted-foreground">Docker image for Grype scanner. Pin to a specific version for supply chain security.</p>
|
|
</div>
|
|
<div class="space-y-2">
|
|
<Label for="trivy-image">Trivy image</Label>
|
|
<Input
|
|
id="trivy-image"
|
|
value={defaultTrivyImage}
|
|
onblur={handleTrivyImageBlur}
|
|
disabled={!$canAccess('settings', 'edit')}
|
|
placeholder={"aquasec/trivy:0.69.3"}
|
|
/>
|
|
<p class="text-xs text-muted-foreground">Docker image for Trivy scanner. Pin to a specific version for supply chain security.</p>
|
|
</div>
|
|
<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>
|
|
<div class="pt-2 border-t">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<p class="text-sm font-medium">Scanner cache</p>
|
|
<p class="text-xs text-muted-foreground">Remove cached vulnerability databases to free disk space. Next scan will re-download fresh data (~200MB).</p>
|
|
</div>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={clearingCache || !$canAccess('settings', 'edit')}
|
|
onclick={clearScannerCache}
|
|
>
|
|
{#if clearingCache}
|
|
Clearing...
|
|
{:else}
|
|
Clear cache
|
|
{/if}
|
|
</Button>
|
|
</div>
|
|
</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-3">
|
|
<div>
|
|
<div class="flex items-center gap-2">
|
|
<Label>Activity event collection mode</Label>
|
|
<Tooltip.Root>
|
|
<Tooltip.Trigger>
|
|
<HelpCircle class="w-3.5 h-3.5 text-muted-foreground" />
|
|
</Tooltip.Trigger>
|
|
<Tooltip.Content class="w-80">
|
|
<p class="text-xs">
|
|
<strong>Stream:</strong> Continuous event stream from Docker, instant notifications, higher CPU usage<br />
|
|
<strong>Poll:</strong> Periodic checks for new events, slight notification delay, lower CPU usage
|
|
</p>
|
|
</Tooltip.Content>
|
|
</Tooltip.Root>
|
|
</div>
|
|
<div class="flex items-center gap-4 mt-2">
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
<input
|
|
type="radio"
|
|
name="eventCollectionMode"
|
|
value="stream"
|
|
checked={(eventCollectionMode || 'stream') === 'stream'}
|
|
onchange={() => handleEventCollectionModeChange('stream')}
|
|
disabled={!$canAccess('settings', 'edit')}
|
|
class="accent-primary w-4 h-4"
|
|
/>
|
|
<Activity class="w-3.5 h-3.5" />
|
|
<span class="text-sm">Stream</span>
|
|
</label>
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
<input
|
|
type="radio"
|
|
name="eventCollectionMode"
|
|
value="poll"
|
|
checked={(eventCollectionMode || 'stream') === 'poll'}
|
|
onchange={() => handleEventCollectionModeChange('poll')}
|
|
disabled={!$canAccess('settings', 'edit')}
|
|
class="accent-primary w-4 h-4"
|
|
/>
|
|
<Clock class="w-3.5 h-3.5" />
|
|
<span class="text-sm">Poll</span>
|
|
</label>
|
|
|
|
<span class="text-xs text-muted-foreground {(eventCollectionMode || 'stream') === 'poll' ? '' : 'invisible'}">every</span>
|
|
<Select.Root
|
|
type="single"
|
|
value={String(eventPollInterval || 60000)}
|
|
onValueChange={(v) => v && handleEventPollIntervalChange({ value: parseInt(v) })}
|
|
disabled={!$canAccess('settings', 'edit') || (eventCollectionMode || 'stream') !== 'poll'}
|
|
>
|
|
<Select.Trigger class="w-24 h-8 {(eventCollectionMode || 'stream') === 'poll' ? '' : 'invisible'}">
|
|
{(eventPollInterval || 60000) === 30000 ? '30s' : (eventPollInterval || 60000) === 60000 ? '60s' : (eventPollInterval || 60000) === 120000 ? '120s' : '300s'}
|
|
</Select.Trigger>
|
|
<Select.Content>
|
|
<Select.Item value="30000">30s</Select.Item>
|
|
<Select.Item value="60000">60s</Select.Item>
|
|
<Select.Item value="120000">120s</Select.Item>
|
|
<Select.Item value="300000">300s</Select.Item>
|
|
</Select.Content>
|
|
</Select.Root>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-1 pt-2 border-t">
|
|
<div class="flex items-center gap-2">
|
|
<Label for="metrics-interval">Metrics collection interval</Label>
|
|
<Tooltip.Root>
|
|
<Tooltip.Trigger>
|
|
<HelpCircle class="w-3.5 h-3.5 text-muted-foreground" />
|
|
</Tooltip.Trigger>
|
|
<Tooltip.Content class="w-80">
|
|
<p class="text-xs">
|
|
How often to collect CPU/memory metrics from running containers. Lower intervals
|
|
provide more frequent updates but increase CPU usage.
|
|
</p>
|
|
</Tooltip.Content>
|
|
</Tooltip.Root>
|
|
</div>
|
|
<div class="flex items-center gap-2 mt-2">
|
|
<Select.Root
|
|
type="single"
|
|
value={String(metricsCollectionInterval || 30000)}
|
|
onValueChange={(v) => v && handleMetricsIntervalChange({ value: parseInt(v) })}
|
|
disabled={!$canAccess('settings', 'edit')}
|
|
>
|
|
<Select.Trigger class="w-24 h-8">
|
|
{(metricsCollectionInterval || 30000) === 10000 ? '10s' : (metricsCollectionInterval || 30000) === 30000 ? '30s' : (metricsCollectionInterval || 30000) === 60000 ? '60s' : '120s'}
|
|
</Select.Trigger>
|
|
<Select.Content>
|
|
<Select.Item value="10000">10s</Select.Item>
|
|
<Select.Item value="30000">30s</Select.Item>
|
|
<Select.Item value="60000">60s</Select.Item>
|
|
<Select.Item value="120000">120s</Select.Item>
|
|
</Select.Content>
|
|
</Select.Root>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-1 pt-2 border-t">
|
|
<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>
|