From f2fbca17b719f72c139a6e18c420ca8bddfbdb98 Mon Sep 17 00:00:00 2001 From: SaelixCode Date: Thu, 5 Mar 2026 14:55:04 -0500 Subject: [PATCH] feat: implement dynamic volumes, custom env vars, and timezone detection --- CHANGELOG.md | 4 + backend/src/services/TemplateService.ts | 6 -- frontend/src/components/AppStoreView.tsx | 99 +++++++++++++++++++++--- 3 files changed, 92 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a48fb100..12576f8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +- **Changed:** Removed rigid volume sanitization, allowing full user control over bind paths. +- **Added:** Editable Host Volumes in the deployment UI. +- **Added:** Custom Environment Variable injection tool. +- **Fixed:** ScrollArea UI height rendering and dynamic browser timezone detection. - **Changed:** Rebranded "Templates" to "App Store" across the UI. - **Added:** Advanced deployment configuration panel (Editable Ports and Environment Variables) with smart defaults. - **Fixed:** Implemented smart image fallbacks for broken registry logos and added expandable descriptions. diff --git a/backend/src/services/TemplateService.ts b/backend/src/services/TemplateService.ts index d8feac36..fd23c86c 100644 --- a/backend/src/services/TemplateService.ts +++ b/backend/src/services/TemplateService.ts @@ -147,12 +147,6 @@ export class TemplateService { continue; } - if (hostPath.includes('portainer/Files') || hostPath.includes('/your/') || hostPath.includes('/path/to/')) { - const containerFolder = containerPath.split('/').filter(Boolean).pop() || 'data'; - hostPath = `./${containerFolder}`; - } else if (hostPath.startsWith('/')) { - hostPath = hostPath.replace(/^\//, './'); - } yaml += ` - ${hostPath}:${containerPath}${options}\n`; } diff --git a/frontend/src/components/AppStoreView.tsx b/frontend/src/components/AppStoreView.tsx index 4ab9ac4a..a5507037 100644 --- a/frontend/src/components/AppStoreView.tsx +++ b/frontend/src/components/AppStoreView.tsx @@ -48,6 +48,10 @@ export function AppStoreView({ onDeploySuccess }: AppStoreViewProps) { const [imgErrors, setImgErrors] = useState>({}); const [portVars, setPortVars] = useState>({}); const [isDescExpanded, setIsDescExpanded] = useState(false); + const [volVars, setVolVars] = useState>({}); + const [customEnvs, setCustomEnvs] = useState>([]); + const [newEnvKey, setNewEnvKey] = useState(''); + const [newEnvVal, setNewEnvVal] = useState(''); useEffect(() => { fetchTemplates(); @@ -68,17 +72,33 @@ export function AppStoreView({ onDeploySuccess }: AppStoreViewProps) { const handleSelectTemplate = (t: Template) => { setSelectedTemplate(t); - // Init env vars with defaults + const localEnvs = [...(t.env || [])]; + // Inject LSIO standards if missing from the API + if (!localEnvs.find(e => e.name === 'PUID')) localEnvs.push({ name: 'PUID', label: 'User ID (PUID)', default: '1000' }); + if (!localEnvs.find(e => e.name === 'PGID')) localEnvs.push({ name: 'PGID', label: 'Group ID (PGID)', default: '1000' }); + if (!localEnvs.find(e => e.name === 'TZ')) { + const browserTz = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'; + localEnvs.push({ name: 'TZ', label: 'Timezone', default: browserTz }); + } + const initEnvs: Record = {}; - t.env?.forEach(e => { - let defaultVal = e.default || ''; - // Smart Defaults - if (e.name === 'PUID' && !defaultVal) defaultVal = '1000'; - if (e.name === 'PGID' && !defaultVal) defaultVal = '1000'; - if (e.name === 'TZ' && !defaultVal) defaultVal = 'America/Toronto'; - initEnvs[e.name] = defaultVal; + localEnvs.forEach(e => { + initEnvs[e.name] = e.default || ''; }); setEnvVars(initEnvs); + t.env = localEnvs; // Mutate local template copy so they render + + // Initialize Volumes + const initVols: Record = {}; + t.volumes?.forEach((v: any) => { + if (v.container) { + initVols[v.container] = v.bind || `./${v.container.split('/').filter(Boolean).pop() || 'data'}`; + } + }); + setVolVars(initVols); + setCustomEnvs([]); // Reset custom envs + setNewEnvKey(''); + setNewEnvVal(''); const initPorts: Record = {}; t.ports?.forEach(p => { @@ -119,6 +139,22 @@ export function AppStoreView({ onDeploySuccess }: AppStoreViewProps) { }); } + // Process Volumes + if (modifiedTemplate.volumes) { + modifiedTemplate.volumes = modifiedTemplate.volumes.map((v: any) => { + if (v.container && volVars[v.container] !== undefined) { + return { ...v, bind: volVars[v.container] }; + } + return v; + }); + } + + // Merge envs + const finalEnvVars = { ...envVars }; + customEnvs.forEach(ce => { + if (ce.key.trim()) finalEnvVars[ce.key.trim()] = ce.value; + }); + try { const res = await fetch('/api/templates/deploy', { method: 'POST', @@ -126,7 +162,7 @@ export function AppStoreView({ onDeploySuccess }: AppStoreViewProps) { body: JSON.stringify({ stackName: stackName.trim(), template: modifiedTemplate, - envVars + envVars: finalEnvVars }) }); const data = await res.json(); @@ -215,7 +251,7 @@ export function AppStoreView({ onDeploySuccess }: AppStoreViewProps) { - + {selectedTemplate && (
@@ -276,7 +312,7 @@ export function AppStoreView({ onDeploySuccess }: AppStoreViewProps) {
- +
)} + {selectedTemplate.volumes && selectedTemplate.volumes.length > 0 && ( +
+

Volumes (Host : Container)

+ {selectedTemplate.volumes.map((v: any, idx: number) => { + if (!v.container) return null; + return ( +
+ + setVolVars(prev => ({ ...prev, [v.container]: e.target.value }))} + placeholder={`/path/to/host/dir`} + /> +
+ ); + })} +
+ )} + {selectedTemplate.env && selectedTemplate.env.length > 0 && (

Environment Variables

@@ -334,6 +389,28 @@ export function AppStoreView({ onDeploySuccess }: AppStoreViewProps) { ))}
)} + +
+

Add Custom Variable

+ {customEnvs.map((ce, idx) => ( +
+ + + +
+ ))} +
+ setNewEnvKey(e.target.value)} className="w-1/3 font-mono text-xs" /> + setNewEnvVal(e.target.value)} className="flex-1 font-mono text-xs" /> + +
+