v1.0.22
This commit is contained in:
1 parent
aefa5e7925
commit
b55e1e5aad
107 files changed
+2193
-443
No files matched your search
@@ -6,7 +6,7 @@
|
||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||
import { TogglePill } from '$lib/components/ui/toggle-pill';
|
||||
import { Shield, Pencil, Plus, Check, RefreshCw, Box, Image, HardDrive, Cable, Layers, Globe, Download, Bell, Sliders, Settings, Users, Eye, SquarePlus, Play, Square, RotateCcw, Trash2, Terminal, ScrollText, Search, Upload, Plug, Unplug, Copy, GitBranch, KeyRound, Building2, Container, TriangleAlert, ClipboardList, Activity, Timer } from 'lucide-svelte';
|
||||
import { getIconComponent } from '$lib/utils/icons';
|
||||
import EnvironmentIcon from '$lib/components/EnvironmentIcon.svelte';
|
||||
import * as Alert from '$lib/components/ui/alert';
|
||||
import { focusFirstInput } from '$lib/utils';
|
||||
|
||||
@@ -546,13 +546,12 @@
|
||||
{#if !formAllEnvironments}
|
||||
<div class="mt-3 grid grid-cols-3 sm:grid-cols-4 lg:grid-cols-6 gap-2">
|
||||
{#each environments as env}
|
||||
{@const EnvIcon = getIconComponent(env.icon || 'globe')}
|
||||
<label class="flex items-center gap-2 p-2 border rounded-md cursor-pointer hover:bg-muted/50 transition-colors text-xs {formEnvironmentIds.includes(env.id) ? 'border-primary bg-primary/5' : ''}">
|
||||
<Checkbox
|
||||
checked={formEnvironmentIds.includes(env.id)}
|
||||
onCheckedChange={() => toggleEnvironment(env.id)}
|
||||
/>
|
||||
<EnvIcon class="w-3.5 h-3.5 flex-shrink-0 text-muted-foreground" />
|
||||
<EnvironmentIcon icon={env.icon || 'globe'} envId={env.id} class="w-3.5 h-3.5 flex-shrink-0 text-muted-foreground" />
|
||||
<span class="truncate">{env.name}</span>
|
||||
</label>
|
||||
{/each}
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
import { canAccess } from '$lib/stores/auth';
|
||||
import { licenseStore } from '$lib/stores/license';
|
||||
import RoleModal from './RoleModal.svelte';
|
||||
import { getIconComponent } from '$lib/utils/icons';
|
||||
import EnvironmentIcon from '$lib/components/EnvironmentIcon.svelte';
|
||||
|
||||
interface Role {
|
||||
id: number;
|
||||
@@ -356,9 +356,8 @@
|
||||
.map(id => environments.find(e => e.id === id))
|
||||
.filter(Boolean)}
|
||||
{#each envs as env}
|
||||
{@const EnvIcon = getIconComponent(env.icon || 'globe')}
|
||||
<span class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs border bg-indigo-500/15 text-indigo-700 dark:text-indigo-400 border-indigo-500/30">
|
||||
<EnvIcon class="w-3 h-3" />
|
||||
<EnvironmentIcon icon={env.icon || 'globe'} envId={env.id} class="w-3 h-3" />
|
||||
{env.name}
|
||||
</span>
|
||||
{/each}
|
||||
|
||||
@@ -59,12 +59,16 @@
|
||||
Tags,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
XCircle
|
||||
XCircle,
|
||||
ImageUp
|
||||
} from 'lucide-svelte';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import * as Alert from '$lib/components/ui/alert';
|
||||
import * as Popover from '$lib/components/ui/popover';
|
||||
import IconPicker from '$lib/components/icon-picker.svelte';
|
||||
import AvatarCropper from '$lib/components/AvatarCropper.svelte';
|
||||
import { isCustomIcon } from '$lib/utils/icons';
|
||||
import EnvironmentIcon from '$lib/components/EnvironmentIcon.svelte';
|
||||
import CronEditor from '$lib/components/cron-editor.svelte';
|
||||
import TimezoneSelector from '$lib/components/TimezoneSelector.svelte';
|
||||
import { whale } from '@lucide/lab';
|
||||
@@ -259,6 +263,11 @@
|
||||
let formTlsKey = $state('');
|
||||
let formTlsSkipVerify = $state(false);
|
||||
let formIcon = $state('globe');
|
||||
let pendingIconData = $state<string | null>(null);
|
||||
let iconCropperImageUrl = $state('');
|
||||
let showIconCropper = $state(false);
|
||||
let iconFileInput: HTMLInputElement;
|
||||
let iconCacheBust = $state(Date.now());
|
||||
let formSocketPath = $state('/var/run/docker.sock');
|
||||
let formCollectActivity = $state(true);
|
||||
let formCollectMetrics = $state(true);
|
||||
@@ -282,6 +291,66 @@
|
||||
* Clean a PEM certificate - remove leading/trailing whitespace from each line
|
||||
* This handles copy/paste from formatted output with line numbers
|
||||
*/
|
||||
function handleIconFileSelect(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
iconCropperImageUrl = reader.result as string;
|
||||
showIconCropper = true;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
async function handleIconCropSave(dataUrl: string) {
|
||||
showIconCropper = false;
|
||||
if (environment) {
|
||||
// Edit mode: upload immediately
|
||||
const res = await fetch(`/api/environments/${environment.id}/icon`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ image: dataUrl })
|
||||
});
|
||||
if (res.ok) {
|
||||
const result = await res.json();
|
||||
formIcon = result.icon;
|
||||
iconCacheBust = Date.now();
|
||||
pendingIconData = null;
|
||||
} else {
|
||||
toast.error('Failed to upload icon');
|
||||
}
|
||||
} else {
|
||||
// Create mode: store for later upload after environment is created
|
||||
pendingIconData = dataUrl;
|
||||
formIcon = 'custom:pending';
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadPendingIcon(envId: number) {
|
||||
if (!pendingIconData) return;
|
||||
await fetch(`/api/environments/${envId}/icon`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ image: pendingIconData })
|
||||
});
|
||||
pendingIconData = null;
|
||||
}
|
||||
|
||||
async function removeCustomIcon() {
|
||||
if (environment) {
|
||||
const res = await fetch(`/api/environments/${environment.id}/icon`, { method: 'DELETE' });
|
||||
if (res.ok) {
|
||||
formIcon = 'globe';
|
||||
iconCacheBust = Date.now();
|
||||
}
|
||||
} else {
|
||||
pendingIconData = null;
|
||||
formIcon = 'globe';
|
||||
}
|
||||
}
|
||||
|
||||
function cleanCertificate(cert: string | undefined): string | undefined {
|
||||
if (!cert) return undefined;
|
||||
const cleaned = cert
|
||||
@@ -367,6 +436,8 @@
|
||||
let scannerAvailability = $state<{ grype: boolean; trivy: boolean }>({ grype: false, trivy: false });
|
||||
let scannerVersions = $state<{ grype: string | null; trivy: string | null }>({ grype: null, trivy: null });
|
||||
let scannerLoading = $state(true);
|
||||
let scannerGrypeImage = $state('anchore/grype:v0.110.0');
|
||||
let scannerTrivyImage = $state('aquasec/trivy:0.69.3');
|
||||
let loadingScannerVersions = $state(false);
|
||||
let removingGrype = $state(false);
|
||||
let removingTrivy = $state(false);
|
||||
@@ -454,6 +525,9 @@
|
||||
newLabelInput = '';
|
||||
formPublicIp = environment.publicIp || '';
|
||||
modalTab = 'general';
|
||||
// Reset icon state
|
||||
pendingIconData = null;
|
||||
iconCacheBust = Date.now();
|
||||
// Reset token state for this environment (important when switching between envs)
|
||||
hawserToken = null;
|
||||
generatedToken = null;
|
||||
@@ -479,6 +553,7 @@
|
||||
formTlsKey = '';
|
||||
formTlsSkipVerify = false;
|
||||
formIcon = 'globe';
|
||||
pendingIconData = null;
|
||||
formSocketPath = '/var/run/docker.sock';
|
||||
formCollectActivity = true;
|
||||
formCollectMetrics = true;
|
||||
@@ -668,7 +743,7 @@
|
||||
tlsCert: cleanCertificate(formTlsCert),
|
||||
tlsKey: cleanCertificate(formTlsKey),
|
||||
tlsSkipVerify: formTlsSkipVerify,
|
||||
icon: formIcon,
|
||||
icon: pendingIconData ? 'globe' : formIcon,
|
||||
socketPath: formConnectionType === 'socket' ? formSocketPath : undefined,
|
||||
collectActivity: formCollectActivity,
|
||||
collectMetrics: formCollectMetrics,
|
||||
@@ -682,6 +757,10 @@
|
||||
|
||||
if (response.ok) {
|
||||
const newEnv = await response.json();
|
||||
// Upload pending custom icon if set
|
||||
if (pendingIconData && newEnv?.id) {
|
||||
await uploadPendingIcon(newEnv.id);
|
||||
}
|
||||
// If scanner was enabled, save scanner settings for the new environment
|
||||
if (formEnableScanner && newEnv?.id) {
|
||||
scannerEnabled = true;
|
||||
@@ -876,11 +955,15 @@
|
||||
|
||||
async function saveTimezone(envId: number) {
|
||||
try {
|
||||
await fetch(`/api/environments/${envId}/timezone`, {
|
||||
const response = await fetch(`/api/environments/${envId}/timezone`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ timezone: formTimezone })
|
||||
});
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}));
|
||||
console.error('Failed to save timezone:', data.error || response.status);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save timezone:', error);
|
||||
}
|
||||
@@ -898,6 +981,8 @@
|
||||
const savedScanner = settingsData.settings.scanner || 'none';
|
||||
scannerEnabled = savedScanner !== 'none';
|
||||
selectedScanner = savedScanner === 'none' ? 'both' : savedScanner;
|
||||
if (settingsData.settings.grypeImage) scannerGrypeImage = settingsData.settings.grypeImage;
|
||||
if (settingsData.settings.trivyImage) scannerTrivyImage = settingsData.settings.trivyImage;
|
||||
}
|
||||
scannerLoading = false;
|
||||
loadScannerVersionsAsync(envId);
|
||||
@@ -1122,7 +1207,7 @@
|
||||
const response = await fetch(pullUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ image: 'anchore/grype:latest' })
|
||||
body: JSON.stringify({ image: scannerGrypeImage })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -1154,7 +1239,7 @@
|
||||
const response = await fetch(pullUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ image: 'aquasec/trivy:latest' })
|
||||
body: JSON.stringify({ image: scannerTrivyImage })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -1382,7 +1467,23 @@
|
||||
<div class="space-y-2">
|
||||
<Label for="edit-env-name">Name</Label>
|
||||
<div class="flex gap-2">
|
||||
<IconPicker value={formIcon} onchange={(icon) => formIcon = icon} />
|
||||
{#if isCustomIcon(formIcon) || pendingIconData}
|
||||
<Button variant="outline" size="sm" class="h-9 w-9 p-0 relative group" type="button" onclick={() => iconFileInput?.click()}>
|
||||
{#if pendingIconData}
|
||||
<img src={pendingIconData} alt="" class="w-5 h-5 rounded object-cover" />
|
||||
{:else if environment}
|
||||
<EnvironmentIcon icon={formIcon} envId={environment.id} class="w-5 h-5" cacheBust={iconCacheBust} />
|
||||
{/if}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" class="h-9 w-9 p-0" type="button" title="Remove custom icon" onclick={removeCustomIcon}>
|
||||
<X class="w-3.5 h-3.5 text-muted-foreground" />
|
||||
</Button>
|
||||
{:else}
|
||||
<IconPicker value={formIcon} onchange={(icon) => formIcon = icon} />
|
||||
<Button variant="ghost" size="sm" class="h-9 w-9 p-0" type="button" title="Upload custom icon" onclick={() => iconFileInput?.click()}>
|
||||
<ImageUp class="w-4 h-4 text-muted-foreground" />
|
||||
</Button>
|
||||
{/if}
|
||||
<Input
|
||||
id="edit-env-name"
|
||||
bind:value={formName}
|
||||
@@ -1395,6 +1496,13 @@
|
||||
<p class="text-xs text-destructive">{formErrors.name}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="hidden"
|
||||
bind:this={iconFileInput}
|
||||
onchange={handleIconFileSelect}
|
||||
/>
|
||||
|
||||
<!-- Labels section -->
|
||||
<div class="space-y-2">
|
||||
@@ -2192,7 +2300,7 @@
|
||||
{/if}
|
||||
{#if !loadingScannerVersions}
|
||||
{#if !scannerAvailability.grype}
|
||||
<ImagePullProgressPopover imageName="anchore/grype:latest" envId={environment?.id} onComplete={() => reloadScannerAvailability(environment?.id)}>
|
||||
<ImagePullProgressPopover imageName={scannerGrypeImage} envId={environment?.id} onComplete={() => reloadScannerAvailability(environment?.id)}>
|
||||
<button class="inline-flex items-center text-2xs px-1.5 py-0 h-4 rounded-full border bg-muted/50 hover:bg-muted text-muted-foreground hover:text-foreground transition-colors">
|
||||
<Download class="w-2.5 h-2.5 mr-0.5" />
|
||||
Pull
|
||||
@@ -2269,7 +2377,7 @@
|
||||
{/if}
|
||||
{#if !loadingScannerVersions}
|
||||
{#if !scannerAvailability.trivy}
|
||||
<ImagePullProgressPopover imageName="aquasec/trivy:latest" envId={environment?.id} onComplete={() => reloadScannerAvailability(environment?.id)}>
|
||||
<ImagePullProgressPopover imageName={scannerTrivyImage} envId={environment?.id} onComplete={() => reloadScannerAvailability(environment?.id)}>
|
||||
<button class="inline-flex items-center text-2xs px-1.5 py-0 h-4 rounded-full border bg-muted/50 hover:bg-muted text-muted-foreground hover:text-foreground transition-colors">
|
||||
<Download class="w-2.5 h-2.5 mr-0.5" />
|
||||
Pull
|
||||
@@ -2606,5 +2714,17 @@
|
||||
{/if}
|
||||
</div>
|
||||
</Dialog.Footer>
|
||||
<AvatarCropper
|
||||
show={showIconCropper}
|
||||
imageUrl={iconCropperImageUrl}
|
||||
cropShape="round"
|
||||
outputSize={128}
|
||||
outputFormat="image/webp"
|
||||
outputQuality={0.85}
|
||||
title="Crop icon"
|
||||
saveLabel="Save icon"
|
||||
onCancel={() => showIconCropper = false}
|
||||
onSave={handleIconCropSave}
|
||||
/>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -29,7 +29,7 @@
|
||||
import { broom, whale } from '@lucide/lab';
|
||||
import ConfirmPopover from '$lib/components/ConfirmPopover.svelte';
|
||||
import { canAccess } from '$lib/stores/auth';
|
||||
import { getIconComponent } from '$lib/utils/icons';
|
||||
import EnvironmentIcon from '$lib/components/EnvironmentIcon.svelte';
|
||||
import { getLabelColors } from '$lib/utils/label-colors';
|
||||
import EnvironmentModal from './EnvironmentModal.svelte';
|
||||
import { environments as environmentsStore } from '$lib/stores/environment';
|
||||
@@ -380,12 +380,11 @@
|
||||
{@const testResult = testResults[env.id]}
|
||||
{@const isTesting = testingEnvs.has(env.id)}
|
||||
{@const hasScannerEnabled = envScannerStatus[env.id]}
|
||||
{@const EnvIcon = getIconComponent(env.icon || 'globe')}
|
||||
<Table.Row>
|
||||
<!-- Name Column -->
|
||||
<Table.Cell>
|
||||
<div class="flex items-center gap-2">
|
||||
<EnvIcon class="w-4 h-4 text-muted-foreground shrink-0" />
|
||||
<EnvironmentIcon icon={env.icon || 'globe'} envId={env.id} class="w-4 h-4 text-muted-foreground shrink-0" />
|
||||
{#if env.connectionType === 'socket' || !env.connectionType}
|
||||
<span title="Unix socket connection" class="shrink-0">
|
||||
<Unplug class="w-3.5 h-3.5 text-cyan-500 glow-cyan" />
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
{ id: 'container_restarted', label: 'Container restarted', description: 'When a container restarts' },
|
||||
{ id: 'container_exited', label: 'Container exited', description: 'When a container exits unexpectedly' },
|
||||
{ id: 'container_unhealthy', label: 'Container unhealthy', description: 'When a container health check fails' },
|
||||
{ id: 'container_healthy', label: 'Container healthy', description: 'When a container health check recovers' },
|
||||
{ id: 'container_oom', label: 'Container OOM killed', description: 'When a container is killed due to out of memory' },
|
||||
{ id: 'container_updated', label: 'Container updated', description: 'When a container image is updated' }
|
||||
]
|
||||
@@ -108,6 +109,8 @@
|
||||
icon: HardDrive,
|
||||
events: [
|
||||
{ id: 'image_pulled', label: 'Image pulled', description: 'When a new image is pulled' },
|
||||
{ id: 'image_prune_success', label: 'Image prune completed', description: 'Scheduled image prune completed successfully' },
|
||||
{ id: 'image_prune_failed', label: 'Image prune failed', description: 'Scheduled image prune failed' },
|
||||
{ id: 'environment_offline', label: 'Environment offline', description: 'Environment became unreachable' },
|
||||
{ id: 'environment_online', label: 'Environment online', description: 'Environment came back online' },
|
||||
{ id: 'disk_space_warning', label: 'Disk space warning', description: 'Docker disk usage exceeds threshold' }
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
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 } from 'lucide-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';
|
||||
@@ -25,6 +25,8 @@
|
||||
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);
|
||||
@@ -32,6 +34,7 @@
|
||||
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);
|
||||
@@ -76,6 +79,22 @@
|
||||
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) {
|
||||
@@ -313,45 +332,69 @@
|
||||
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>
|
||||
<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>
|
||||
<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 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>
|
||||
{/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>
|
||||
@@ -368,6 +411,28 @@
|
||||
</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
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { onMount } from 'svelte';
|
||||
import { getIconComponent } from '$lib/utils/icons';
|
||||
import EnvironmentIcon from '$lib/components/EnvironmentIcon.svelte';
|
||||
|
||||
interface RunningStackInfo {
|
||||
envId: number;
|
||||
@@ -121,7 +121,6 @@
|
||||
|
||||
const defaultEnv = $derived(environments.find(e => e.id === defaultEnvId));
|
||||
const defaultEnvName = $derived(defaultEnv?.name || 'Select environment');
|
||||
const DefaultEnvIcon = $derived(defaultEnv ? getIconComponent(defaultEnv.icon || 'globe') : null);
|
||||
|
||||
function isSelected(composePath: string): boolean {
|
||||
return stackSelections.has(composePath);
|
||||
@@ -338,17 +337,16 @@
|
||||
}}
|
||||
>
|
||||
<Select.Trigger class="w-[220px]">
|
||||
{#if DefaultEnvIcon}
|
||||
<DefaultEnvIcon class="w-4 h-4 mr-2 shrink-0" />
|
||||
{#if defaultEnv}
|
||||
<EnvironmentIcon icon={defaultEnv.icon || 'globe'} envId={defaultEnv.id} class="w-4 h-4 mr-2 shrink-0" />
|
||||
{/if}
|
||||
<span class="truncate">{defaultEnvName}</span>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each environments as env}
|
||||
{@const EnvIcon = getIconComponent(env.icon || 'globe')}
|
||||
<Select.Item value={env.id.toString()}>
|
||||
<div class="flex items-center gap-2">
|
||||
<EnvIcon class="w-4 h-4 shrink-0" />
|
||||
<EnvironmentIcon icon={env.icon || 'globe'} envId={env.id} class="w-4 h-4 shrink-0" />
|
||||
<span>{env.name}</span>
|
||||
</div>
|
||||
</Select.Item>
|
||||
@@ -455,17 +453,16 @@
|
||||
>
|
||||
<Select.Trigger class="h-8 w-[220px] text-xs">
|
||||
{#if getStackEnv(stack.composePath)}
|
||||
{@const StackIcon = getIconComponent(getStackEnv(stack.composePath)?.icon || 'globe')}
|
||||
<StackIcon class="w-3.5 h-3.5 mr-1.5 shrink-0" />
|
||||
{@const stackEnv = getStackEnv(stack.composePath)}
|
||||
<EnvironmentIcon icon={stackEnv?.icon || 'globe'} envId={stackEnv?.id || 0} class="w-3.5 h-3.5 mr-1.5 shrink-0" />
|
||||
{/if}
|
||||
<span class="truncate">{getStackEnv(stack.composePath)?.name || 'Select'}</span>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each environments as env}
|
||||
{@const EnvIcon = getIconComponent(env.icon || 'globe')}
|
||||
<Select.Item value={env.id.toString()}>
|
||||
<div class="flex items-center gap-2">
|
||||
<EnvIcon class="w-3.5 h-3.5 shrink-0" />
|
||||
<EnvironmentIcon icon={env.icon || 'globe'} envId={env.id} class="w-3.5 h-3.5 shrink-0" />
|
||||
<span>{env.name}</span>
|
||||
</div>
|
||||
</Select.Item>
|
||||
@@ -490,15 +487,12 @@
|
||||
<div class="space-y-1.5">
|
||||
{#each adoptedStacks as adopted}
|
||||
{@const env = environments.find(e => e.id === adopted.envId)}
|
||||
{@const EnvIcon = env ? getIconComponent(env.icon || 'globe') : null}
|
||||
<div class="flex items-center gap-2 p-2 rounded-md bg-green-500/10 border border-green-500/20">
|
||||
<CheckCircle2 class="w-4 h-4 text-green-600 dark:text-green-500 shrink-0" />
|
||||
<p class="text-sm font-medium flex-1">{adopted.name}</p>
|
||||
{#if env}
|
||||
<div class="flex items-center gap-1.5 text-xs text-muted-foreground shrink-0">
|
||||
{#if EnvIcon}
|
||||
<EnvIcon class="w-3.5 h-3.5" />
|
||||
{/if}
|
||||
<EnvironmentIcon icon={env.icon || 'globe'} envId={env.id} class="w-3.5 h-3.5" />
|
||||
<span>{env.name}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in new issue
Block a user