863 lines
32 KiB
Svelte
863 lines
32 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, Save, RotateCcw, LayoutDashboard, Tags } from 'lucide-svelte';
|
|
import CodeEditor from '$lib/components/CodeEditor.svelte';
|
|
import { appSettings, type DateFormat, type DownloadFormat, type EventCollectionMode, type LabelFilterMode } 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 showExposedPorts = $derived($appSettings.showExposedPorts);
|
|
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 defaultComposeTemplate = $derived($appSettings.defaultComposeTemplate);
|
|
let labelFilterMode = $derived($appSettings.labelFilterMode);
|
|
let composeTemplateWIP = $state('');
|
|
let composeTemplateInitialized = false;
|
|
|
|
$effect(() => {
|
|
if (!composeTemplateInitialized && defaultComposeTemplate !== undefined) {
|
|
composeTemplateWIP = defaultComposeTemplate;
|
|
composeTemplateInitialized = true;
|
|
}
|
|
});
|
|
|
|
const builtinComposeTemplate = `version: "3.8"
|
|
|
|
services:
|
|
app:
|
|
image: nginx:alpine
|
|
ports:
|
|
- "8080:80"
|
|
environment:
|
|
- APP_ENV=\${APP_ENV:-production}
|
|
volumes:
|
|
- ./html:/usr/share/nginx/html:ro
|
|
restart: unless-stopped
|
|
|
|
# Add more services as needed
|
|
# networks:
|
|
# default:
|
|
# driver: bridge
|
|
`;
|
|
|
|
function saveComposeTemplate() {
|
|
appSettings.setDefaultComposeTemplate(composeTemplateWIP);
|
|
toast.success('Compose template updated');
|
|
}
|
|
|
|
function revertComposeTemplate() {
|
|
composeTemplateWIP = builtinComposeTemplate;
|
|
toast.info('Template reverted to default');
|
|
}
|
|
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 scannerCleanupCron = $derived($appSettings.scannerCleanupCron);
|
|
let scannerCleanupEnabled = $derived($appSettings.scannerCleanupEnabled);
|
|
let logMaxLines = $derived($appSettings.logMaxLines);
|
|
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() {
|
|
const newState = !scheduleCleanupEnabled;
|
|
appSettings.setScheduleCleanupEnabled(newState);
|
|
toast.success(newState ? 'Schedule cleanup enabled' : 'Schedule cleanup disabled');
|
|
}
|
|
|
|
function handleEventCleanupEnabledChange() {
|
|
const newState = !eventCleanupEnabled;
|
|
appSettings.setEventCleanupEnabled(newState);
|
|
toast.success(newState ? 'Event cleanup enabled' : 'Event cleanup disabled');
|
|
}
|
|
|
|
function handleScannerCleanupCronChange(cron: string) {
|
|
appSettings.setScannerCleanupCron(cron);
|
|
toast.success('Scanner cleanup cron updated');
|
|
}
|
|
|
|
function handleScannerCleanupEnabledChange() {
|
|
const newState = !scannerCleanupEnabled;
|
|
appSettings.setScannerCleanupEnabled(newState);
|
|
toast.success(newState ? 'Scanner cleanup enabled' : 'Scanner cleanup disabled');
|
|
}
|
|
|
|
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');
|
|
}
|
|
}
|
|
|
|
// Anything above 2K starts feeling laggy in browsers without virtualized rendering.
|
|
const logMaxLinesOptions = [
|
|
{ value: '500', label: '500 lines' },
|
|
{ value: '1000', label: '1,000 lines' },
|
|
{ value: '2000', label: '2,000 lines' }
|
|
];
|
|
|
|
function handleLogMaxLinesChange(value: string | undefined) {
|
|
const n = parseInt(value ?? '');
|
|
if (!Number.isFinite(n) || n <= 0) return;
|
|
appSettings.setLogMaxLines(Math.min(2000, Math.max(100, n)));
|
|
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={(checked) => {
|
|
appSettings.setShowStoppedContainers(checked);
|
|
toast.success(checked ? 'Stopped containers shown' : 'Stopped containers hidden');
|
|
}}
|
|
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={(checked) => {
|
|
appSettings.setHighlightUpdates(checked);
|
|
toast.success(checked ? 'Update highlighting enabled' : 'Update highlighting disabled');
|
|
}}
|
|
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={(checked) => {
|
|
appSettings.setCompactPorts(checked);
|
|
toast.success(checked ? 'Compact port display enabled' : 'Showing all ports');
|
|
}}
|
|
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>Show exposed ports</Label>
|
|
<Tooltip.Root>
|
|
<Tooltip.Trigger>
|
|
<HelpCircle class="w-3.5 h-3.5 text-muted-foreground" />
|
|
</Tooltip.Trigger>
|
|
<Tooltip.Content side="top" class="max-w-xs">
|
|
<p>Shows internal container ports (from EXPOSE directives) that are not published to the host. These appear in the container list with an amber badge to distinguish them from published port mappings.</p>
|
|
</Tooltip.Content>
|
|
</Tooltip.Root>
|
|
<TogglePill
|
|
checked={showExposedPorts}
|
|
onchange={(checked) => {
|
|
appSettings.setShowExposedPorts(checked);
|
|
toast.success(checked ? 'Showing exposed ports in container list' : 'Exposed ports hidden from container list');
|
|
}}
|
|
disabled={!$canAccess('settings', 'edit')}
|
|
/>
|
|
</div>
|
|
<p class="text-xs text-muted-foreground">Display internal container ports in the container list grid</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={(checked) => {
|
|
appSettings.setConfirmDestructive(checked);
|
|
toast.success(checked ? 'Confirmations enabled' : 'Confirmations disabled');
|
|
}}
|
|
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-max-lines">Log buffer size</Label>
|
|
<Select.Root
|
|
type="single"
|
|
value={String(logMaxLines)}
|
|
onValueChange={handleLogMaxLinesChange}
|
|
disabled={!$canAccess('settings', 'edit')}
|
|
>
|
|
<Select.Trigger id="log-max-lines" class="w-48">
|
|
{logMaxLines.toLocaleString()} lines
|
|
</Select.Trigger>
|
|
<Select.Content>
|
|
{#each logMaxLinesOptions as opt}
|
|
<Select.Item value={opt.value}>{opt.label}</Select.Item>
|
|
{/each}
|
|
</Select.Content>
|
|
</Select.Root>
|
|
<p class="text-xs text-muted-foreground">Maximum number of log lines kept per container panel. Older lines are dropped when the limit is exceeded.</p>
|
|
</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={(checked) => {
|
|
appSettings.setFormatLogTimestamps(checked);
|
|
toast.success(checked ? 'Log timestamp formatting enabled' : 'Log timestamp formatting disabled');
|
|
}}
|
|
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>
|
|
|
|
<Card.Root>
|
|
<Card.Header>
|
|
<Card.Title class="text-sm font-medium flex items-center gap-2">
|
|
<FileText class="w-4 h-4" />
|
|
Compose template
|
|
</Card.Title>
|
|
<p class="text-xs text-muted-foreground">Default YAML content when creating a new stack.</p>
|
|
</Card.Header>
|
|
<Card.Content class="space-y-3">
|
|
<div class="h-64">
|
|
<CodeEditor
|
|
value={composeTemplateWIP}
|
|
onchange={(v) => { composeTemplateWIP = v; }}
|
|
language="yaml"
|
|
readonly={!$canAccess('settings', 'edit')}
|
|
class="h-full rounded-md overflow-hidden border border-zinc-200 dark:border-zinc-700"
|
|
/>
|
|
</div>
|
|
{#if $canAccess('settings', 'edit')}
|
|
<div class="flex gap-2">
|
|
<Button size="sm" variant="outline" onclick={saveComposeTemplate}>
|
|
<Save class="w-3.5 h-3.5" />
|
|
Save template
|
|
</Button>
|
|
<Button size="sm" variant="ghost" onclick={revertComposeTemplate}>
|
|
<RotateCcw class="w-3.5 h-3.5" />
|
|
Revert to default
|
|
</Button>
|
|
</div>
|
|
{/if}
|
|
</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>
|
|
<div class="space-y-1 pt-2 border-t">
|
|
<div class="flex items-center gap-3">
|
|
<Label>Scanner cache cleanup</Label>
|
|
<TogglePill
|
|
checked={scannerCleanupEnabled}
|
|
onchange={handleScannerCleanupEnabledChange}
|
|
disabled={!$canAccess('settings', 'edit')}
|
|
/>
|
|
</div>
|
|
<p class="text-xs text-muted-foreground">Remove cached vulnerability databases to reclaim disk space</p>
|
|
<div class="flex items-center gap-2 mt-2">
|
|
<div class="ml-auto">
|
|
<CronEditor
|
|
value={scannerCleanupCron}
|
|
onchange={handleScannerCleanupCronChange}
|
|
disabled={!$canAccess('settings', 'edit') || !scannerCleanupEnabled}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card.Content>
|
|
</Card.Root>
|
|
|
|
<Card.Root>
|
|
<Card.Header>
|
|
<Card.Title class="text-sm font-medium flex items-center gap-2">
|
|
<LayoutDashboard class="w-4 h-4" />
|
|
Dashboard
|
|
</Card.Title>
|
|
</Card.Header>
|
|
<Card.Content class="space-y-4">
|
|
<div class="space-y-3">
|
|
<div class="space-y-1">
|
|
<div class="flex items-center gap-3">
|
|
<Label>Label filter matching</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">
|
|
Controls how multiple selected labels filter environments on the dashboard.
|
|
<strong>"Any"</strong>: shows environments that have at least one of the selected labels.
|
|
<strong>"All"</strong>: shows only environments that have every selected label.
|
|
</p>
|
|
</Tooltip.Content>
|
|
</Tooltip.Root>
|
|
<ToggleSwitch
|
|
value={labelFilterMode}
|
|
leftValue="any"
|
|
rightValue="all"
|
|
onchange={(mode) => appSettings.setLabelFilterMode(mode as LabelFilterMode)}
|
|
disabled={!$canAccess('settings', 'edit')}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card.Content>
|
|
</Card.Root>
|
|
</div>
|
|
</div>
|
|
</div>
|