Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/lib/server/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4362,6 +4362,19 @@ export async function setMetricsCollectionInterval(interval: number): Promise<vo
await setSetting('metrics_collection_interval', interval);
}

// =============================================================================
// COMPOSE TEMPLATE SETTINGS
// =============================================================================

export async function getDefaultComposeTemplate(): Promise<string> {
const value = await getSetting('default_compose_template');
return value || '';
}

export async function setDefaultComposeTemplate(template: string): Promise<void> {
await setSetting('default_compose_template', template);
}

// =============================================================================
// STACK ENVIRONMENT VARIABLES OPERATIONS
// =============================================================================
Expand Down
34 changes: 31 additions & 3 deletions src/lib/stores/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface AppSettings {
metricsCollectionInterval: number;
externalStackPaths: string[];
primaryStackLocation: string | null;
defaultComposeTemplate: string;
}

const DEFAULT_SETTINGS: AppSettings = {
Expand All @@ -51,7 +52,25 @@ const DEFAULT_SETTINGS: AppSettings = {
eventPollInterval: 60000,
metricsCollectionInterval: 30000,
externalStackPaths: [],
primaryStackLocation: null
primaryStackLocation: null,
defaultComposeTemplate: `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
`
};

// Create a writable store for app settings
Expand Down Expand Up @@ -89,7 +108,8 @@ function createSettingsStore() {
eventPollInterval: settings.eventPollInterval ?? DEFAULT_SETTINGS.eventPollInterval,
metricsCollectionInterval: settings.metricsCollectionInterval ?? DEFAULT_SETTINGS.metricsCollectionInterval,
externalStackPaths: settings.externalStackPaths ?? DEFAULT_SETTINGS.externalStackPaths,
primaryStackLocation: settings.primaryStackLocation ?? DEFAULT_SETTINGS.primaryStackLocation
primaryStackLocation: settings.primaryStackLocation ?? DEFAULT_SETTINGS.primaryStackLocation,
defaultComposeTemplate: settings.defaultComposeTemplate ?? DEFAULT_SETTINGS.defaultComposeTemplate
});
}
} catch {
Expand Down Expand Up @@ -130,7 +150,8 @@ function createSettingsStore() {
eventPollInterval: updatedSettings.eventPollInterval ?? DEFAULT_SETTINGS.eventPollInterval,
metricsCollectionInterval: updatedSettings.metricsCollectionInterval ?? DEFAULT_SETTINGS.metricsCollectionInterval,
externalStackPaths: updatedSettings.externalStackPaths ?? DEFAULT_SETTINGS.externalStackPaths,
primaryStackLocation: updatedSettings.primaryStackLocation ?? DEFAULT_SETTINGS.primaryStackLocation
primaryStackLocation: updatedSettings.primaryStackLocation ?? DEFAULT_SETTINGS.primaryStackLocation,
defaultComposeTemplate: updatedSettings.defaultComposeTemplate ?? DEFAULT_SETTINGS.defaultComposeTemplate
});
}
} catch (error) {
Expand Down Expand Up @@ -304,6 +325,13 @@ function createSettingsStore() {
return newSettings;
});
},
setDefaultComposeTemplate: (value: string) => {
update((current) => {
const newSettings = { ...current, defaultComposeTemplate: value };
saveSettings({ defaultComposeTemplate: value });
return newSettings;
});
},
// Manual refresh from database
refresh: loadSettings
};
Expand Down
47 changes: 39 additions & 8 deletions src/routes/api/settings/general/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ export interface GeneralSettings {
externalStackPaths: string[];
// Primary stack location
primaryStackLocation: string | null;
// Default compose template
defaultComposeTemplate: string;
}

const DEFAULT_SETTINGS: Omit<GeneralSettings, 'scheduleRetentionDays' | 'eventRetentionDays' | 'scheduleCleanupCron' | 'eventCleanupCron' | 'scheduleCleanupEnabled' | 'eventCleanupEnabled'> = {
Expand All @@ -91,7 +93,27 @@ const DEFAULT_SETTINGS: Omit<GeneralSettings, 'scheduleRetentionDays' | 'eventRe
fontSize: 'normal',
gridFontSize: 'normal',
terminalFont: 'system-mono',
editorFont: 'system-mono'
editorFont: 'system-mono',
defaultComposeTemplate: `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
`,
externalStackPaths: [],
primaryStackLocation: null
};

const VALID_LIGHT_THEMES = ['default', 'catppuccin', 'rose-pine', 'nord', 'solarized', 'gruvbox', 'alucard', 'github', 'material', 'atom-one'];
Expand Down Expand Up @@ -141,7 +163,8 @@ export const GET: RequestHandler = async ({ cookies }) => {
terminalFont,
editorFont,
externalStackPaths,
primaryStackLocation
primaryStackLocation,
defaultComposeTemplate
] = await Promise.all([
getSetting('confirm_destructive'),
getSetting('show_stopped_containers'),
Expand Down Expand Up @@ -170,7 +193,8 @@ export const GET: RequestHandler = async ({ cookies }) => {
getSetting('theme_terminal_font'),
getSetting('theme_editor_font'),
getExternalStackPaths(),
getPrimaryStackLocation()
getPrimaryStackLocation(),
getSetting('default_compose_template')
]);

const settings: GeneralSettings = {
Expand Down Expand Up @@ -201,7 +225,8 @@ export const GET: RequestHandler = async ({ cookies }) => {
terminalFont: terminalFont ?? DEFAULT_SETTINGS.terminalFont,
editorFont: editorFont ?? DEFAULT_SETTINGS.editorFont,
externalStackPaths,
primaryStackLocation
primaryStackLocation,
defaultComposeTemplate: defaultComposeTemplate ?? DEFAULT_SETTINGS.defaultComposeTemplate
};

return json(settings);
Expand All @@ -219,7 +244,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {

try {
const body = await request.json();
const { confirmDestructive, showStoppedContainers, highlightUpdates, timeFormat, dateFormat, downloadFormat, defaultGrypeArgs, defaultTrivyArgs, scheduleRetentionDays, eventRetentionDays, scheduleCleanupCron, eventCleanupCron, scheduleCleanupEnabled, eventCleanupEnabled, logBufferSizeKb, defaultTimezone, eventCollectionMode, eventPollInterval, metricsCollectionInterval, lightTheme, darkTheme, font, fontSize, gridFontSize, terminalFont, editorFont, externalStackPaths, primaryStackLocation } = body;
const { confirmDestructive, showStoppedContainers, highlightUpdates, timeFormat, dateFormat, downloadFormat, defaultGrypeArgs, defaultTrivyArgs, scheduleRetentionDays, eventRetentionDays, scheduleCleanupCron, eventCleanupCron, scheduleCleanupEnabled, eventCleanupEnabled, logBufferSizeKb, defaultTimezone, eventCollectionMode, eventPollInterval, metricsCollectionInterval, lightTheme, darkTheme, font, fontSize, gridFontSize, terminalFont, editorFont, externalStackPaths, primaryStackLocation, defaultComposeTemplate } = body;

if (confirmDestructive !== undefined) {
await setSetting('confirm_destructive', confirmDestructive);
Expand Down Expand Up @@ -326,6 +351,9 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
await setPrimaryStackLocation(null);
}
}
if (defaultComposeTemplate !== undefined && typeof defaultComposeTemplate === 'string') {
await setSetting('default_compose_template', defaultComposeTemplate);
}

// Fetch all settings in parallel for the response
const [
Expand Down Expand Up @@ -356,7 +384,8 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
terminalFontVal,
editorFontVal,
externalStackPathsVal,
primaryStackLocationVal
primaryStackLocationVal,
defaultComposeTemplateVal
] = await Promise.all([
getSetting('confirm_destructive'),
getSetting('show_stopped_containers'),
Expand Down Expand Up @@ -385,7 +414,8 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
getSetting('theme_terminal_font'),
getSetting('theme_editor_font'),
getExternalStackPaths(),
getPrimaryStackLocation()
getPrimaryStackLocation(),
getSetting('default_compose_template')
]);

const settings: GeneralSettings = {
Expand Down Expand Up @@ -416,7 +446,8 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
terminalFont: terminalFontVal ?? DEFAULT_SETTINGS.terminalFont,
editorFont: editorFontVal ?? DEFAULT_SETTINGS.editorFont,
externalStackPaths: externalStackPathsVal,
primaryStackLocation: primaryStackLocationVal
primaryStackLocation: primaryStackLocationVal,
defaultComposeTemplate: defaultComposeTemplateVal ?? DEFAULT_SETTINGS.defaultComposeTemplate
};

return json(settings);
Expand Down
42 changes: 41 additions & 1 deletion src/routes/settings/general/GeneralTab.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@
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, Save } 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';
import CodeEditor from '$lib/components/CodeEditor.svelte';

// General settings state - these derive from the store
let confirmDestructive = $derived($appSettings.confirmDestructive);
Expand All @@ -35,6 +36,8 @@
let eventCollectionMode = $derived($appSettings.eventCollectionMode);
let eventPollInterval = $derived($appSettings.eventPollInterval);
let metricsCollectionInterval = $derived($appSettings.metricsCollectionInterval);
let defaultComposeTemplate = $derived($appSettings.defaultComposeTemplate);
let defaultComposeTemplateWIP = defaultComposeTemplate

const dateFormatOptions: { value: DateFormat; label: string; example: string }[] = [
{ value: 'DD.MM.YYYY', label: 'DD.MM.YYYY', example: '31.12.2024' },
Expand Down Expand Up @@ -117,6 +120,16 @@
toast.success(`Metrics interval: ${selected.value / 1000}s`);
}
}

function handleChangeDefaultComposeTemplate(newString: String) {
defaultComposeTemplateWIP = newString
}

function handleDefaultComposeTemplateSave() {
defaultComposeTemplate = defaultComposeTemplateWIP
appSettings.setDefaultComposeTemplate(defaultComposeTemplate);
toast.success('Compose template updated');
}
</script>

<div class="flex-1 min-h-0 overflow-y-auto">
Expand Down Expand Up @@ -560,6 +573,33 @@
</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">Used as the default yaml content when creating a new stack.</p>
</Card.Header>
<Card.Content class="space-y-4">
<CodeEditor
value={defaultComposeTemplate}
onchange={handleChangeDefaultComposeTemplate}
language="yaml"
theme="dark"
readonly={!$canAccess('settings', 'edit')}
class="h-full rounded-md overflow-hidden border border-zinc-200 dark:border-zinc-700"
/>

{#if $canAccess('settings', 'edit')}
<Button size="sm" variant="outline" onclick={handleDefaultComposeTemplateSave}>
<Save class="w-4 h-4" />
Save Template
</Button>
{/if}
</Card.Content>
</Card.Root>
</div>
</div>
</div>
19 changes: 1 addition & 18 deletions src/routes/stacks/StackModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -558,24 +558,7 @@
// Debounce timer for validation
let validateTimer: ReturnType<typeof setTimeout> | null = null;

const defaultCompose = `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
`;
const defaultCompose = $appSettings.defaultComposeTemplate;

// Count of defined environment variables (with non-empty keys)
const envVarCount = $derived(envVars.filter(v => v.key.trim()).length);
Expand Down