Skip to content

Commit 0496f75

Browse files
authored
style: update deployment configuration ux to include collapsible sections (#329)
1 parent 1a161e3 commit 0496f75

File tree

5 files changed

+222
-104
lines changed

5 files changed

+222
-104
lines changed

view/app/self-host/components/application-details/configuration.tsx

Lines changed: 194 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import { Button } from '@/components/ui/button';
44
import { Form } from '@/components/ui/form';
55
import FormInputField from '@/components/ui/form-input-field';
66
import { FormSelectTagInputField } from '@/components/ui/form-select-tag-field';
7+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
78
import { BuildPack, Environment } from '@/redux/types/deploy-form';
89
import useUpdateDeployment from '../../hooks/use_update_deployment';
910
import { parsePort } from '../../utils/parsePort';
1011
import { useTranslation } from '@/hooks/use-translation';
1112
import { ResourceGuard, AnyPermissionGuard } from '@/components/rbac/PermissionGuard';
1213
import { Skeleton } from '@/components/ui/skeleton';
14+
import { ChevronDownIcon, ChevronRightIcon, SettingsIcon, ServerIcon, CodeIcon, InfoIcon, Terminal } from 'lucide-react';
1315

1416
interface DeployConfigureProps {
1517
application_name?: string;
@@ -28,6 +30,61 @@ interface DeployConfigureProps {
2830
base_path?: string;
2931
}
3032

33+
interface CollapsibleSectionProps {
34+
title: string;
35+
children: React.ReactNode;
36+
defaultOpen?: boolean;
37+
icon?: React.ComponentType<{ size?: number; className?: string }>;
38+
badge?: string;
39+
description?: string;
40+
}
41+
42+
const CollapsibleSection = ({
43+
title,
44+
children,
45+
defaultOpen = false,
46+
icon: Icon,
47+
badge,
48+
description
49+
}: CollapsibleSectionProps) => {
50+
const [isOpen, setIsOpen] = useState(defaultOpen);
51+
52+
return (
53+
<div className="border rounded-lg overflow-hidden">
54+
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
55+
<CollapsibleTrigger className="w-full p-4 flex items-center justify-between hover:bg-muted/50 transition-colors group">
56+
<div className="flex items-center gap-3">
57+
{Icon && (
58+
<Icon size={20} className="text-muted-foreground group-hover:text-foreground transition-colors" />
59+
)}
60+
<div className="text-left">
61+
<h3 className="text-sm font-medium">{title}</h3>
62+
{description && (
63+
<p className="text-xs text-muted-foreground mt-1">{description}</p>
64+
)}
65+
</div>
66+
{badge && (
67+
<span className={`px-2 py-1 ${badge.toLowerCase() === 'required' ? 'bg-destructive/10 text-destructive' : badge.toLowerCase() === 'read-only' ? 'bg-muted/10 text-muted-foreground' : 'bg-primary/10 text-primary'} text-xs rounded-full font-medium`}>
68+
{badge}
69+
</span>
70+
)}
71+
</div>
72+
{isOpen ? (
73+
<ChevronDownIcon className="h-4 w-4 text-muted-foreground transition-transform duration-200" />
74+
) : (
75+
<ChevronRightIcon className="h-4 w-4 text-muted-foreground transition-transform duration-200" />
76+
)}
77+
</CollapsibleTrigger>
78+
<CollapsibleContent className="border-t bg-muted/20">
79+
<div className="p-4 space-y-4">
80+
{children}
81+
</div>
82+
</CollapsibleContent>
83+
</Collapsible>
84+
</div>
85+
);
86+
};
87+
3188
export const DeployConfigureForm = ({
3289
application_name = '',
3390
environment = Environment.Production,
@@ -94,123 +151,156 @@ export const DeployConfigureForm = ({
94151
loadingFallback={null}
95152
>
96153
<Form {...form}>
97-
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
98-
<div className="grid sm:grid-cols-2 gap-4">
99-
<FormInputField
100-
form={form}
101-
label={t('selfHost.configuration.fields.applicationName.label')}
102-
name="name"
103-
description={t('selfHost.configuration.fields.applicationName.description')}
104-
placeholder={t('selfHost.configuration.fields.applicationName.label')}
105-
/>
106-
{build_pack !== BuildPack.Static && (
154+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
155+
<CollapsibleSection
156+
title={t('selfHost.configuration.sections.basicConfiguration')}
157+
icon={SettingsIcon}
158+
badge="Required"
159+
description="Essential settings for your application"
160+
defaultOpen={true}
161+
>
162+
<div className="grid sm:grid-cols-2 gap-4">
107163
<FormInputField
108164
form={form}
109-
label={t('selfHost.configuration.fields.port.label')}
110-
name="port"
111-
description={t('selfHost.configuration.fields.port.description')}
112-
placeholder="3000"
113-
validator={(value) => parsePort(value) !== null}
165+
label={t('selfHost.configuration.fields.applicationName.label')}
166+
name="name"
167+
placeholder={t('selfHost.configuration.fields.applicationName.label')}
114168
/>
115-
)}
116-
</div>
117-
118-
{build_pack !== BuildPack.Static && (
119-
<>
120-
<div className="grid sm:grid-cols-2 gap-4">
121-
<FormInputField
122-
form={form}
123-
label={t('selfHost.configuration.fields.basePath.label')}
124-
name="base_path"
125-
description={t('selfHost.configuration.fields.basePath.description')}
126-
placeholder="/"
127-
required={false}
128-
/>
169+
{build_pack !== BuildPack.Static && (
129170
<FormInputField
130171
form={form}
131-
label={t('selfHost.configuration.fields.dockerfilePath.label')}
132-
name="DockerfilePath"
133-
description={t('selfHost.configuration.fields.dockerfilePath.description')}
134-
placeholder="Dockerfile"
135-
required={false}
172+
label={t('selfHost.configuration.fields.port.label')}
173+
name="port"
174+
placeholder="3000"
175+
validator={(value) => parsePort(value) !== null}
136176
/>
137-
</div>
177+
)}
178+
</div>
179+
</CollapsibleSection>
138180

139-
<div className="grid sm:grid-cols-2 gap-4">
140-
<FormSelectTagInputField
141-
form={form}
142-
label={t('selfHost.configuration.fields.environmentVariables.label')}
143-
name="environment_variables"
144-
description={t('selfHost.configuration.fields.environmentVariables.description')}
145-
placeholder={t('selfHost.configuration.fields.environmentVariables.placeholder')}
146-
required={false}
147-
validator={validateEnvVar}
148-
defaultValues={env_variables}
149-
/>
150-
<FormSelectTagInputField
151-
form={form}
152-
label={t('selfHost.configuration.fields.buildVariables.label')}
153-
name="build_variables"
154-
description={t('selfHost.configuration.fields.buildVariables.description')}
155-
placeholder={t('selfHost.configuration.fields.buildVariables.placeholder')}
156-
required={false}
157-
validator={validateEnvVar}
158-
defaultValues={build_variables}
159-
/>
160-
</div>
181+
{build_pack !== BuildPack.Static && (
182+
<>
183+
<CollapsibleSection
184+
title={t('selfHost.configuration.sections.dockerConfiguration')}
185+
icon={ServerIcon}
186+
badge="Optional"
187+
description="Container and deployment configuration"
188+
defaultOpen={false}
189+
>
190+
<div className="grid sm:grid-cols-2 gap-4">
191+
<FormInputField
192+
form={form}
193+
label={t('selfHost.configuration.fields.basePath.label')}
194+
name="base_path"
195+
placeholder="/"
196+
required={false}
197+
/>
198+
<FormInputField
199+
form={form}
200+
label={t('selfHost.configuration.fields.dockerfilePath.label')}
201+
name="DockerfilePath"
202+
placeholder="Dockerfile"
203+
required={false}
204+
/>
205+
</div>
206+
</CollapsibleSection>
161207

162-
<div className="grid sm:grid-cols-2 gap-4">
163-
<FormInputField
164-
form={form}
165-
label={t('selfHost.configuration.fields.preRunCommands.label')}
166-
name="pre_run_command"
167-
description={t('selfHost.configuration.fields.preRunCommands.description')}
168-
placeholder={t('selfHost.configuration.fields.preRunCommands.placeholder')}
169-
required={false}
170-
/>
171-
<FormInputField
172-
form={form}
173-
label={t('selfHost.configuration.fields.postRunCommands.label')}
174-
name="post_run_command"
175-
description={t('selfHost.configuration.fields.postRunCommands.description')}
176-
placeholder={t('selfHost.configuration.fields.postRunCommands.placeholder')}
177-
required={false}
178-
/>
179-
</div>
208+
<CollapsibleSection
209+
title={t('selfHost.configuration.sections.environmentVariables')}
210+
icon={CodeIcon}
211+
badge="Optional"
212+
description="Runtime and build-time variables"
213+
defaultOpen={false}
214+
>
215+
<div className="grid sm:grid-cols-2 gap-4">
216+
<FormSelectTagInputField
217+
form={form}
218+
label={t('selfHost.configuration.fields.environmentVariables.label')}
219+
name="environment_variables"
220+
placeholder={t('selfHost.configuration.fields.environmentVariables.placeholder')}
221+
required={false}
222+
validator={validateEnvVar}
223+
defaultValues={env_variables}
224+
/>
225+
<FormSelectTagInputField
226+
form={form}
227+
label={t('selfHost.configuration.fields.buildVariables.label')}
228+
name="build_variables"
229+
placeholder={t('selfHost.configuration.fields.buildVariables.placeholder')}
230+
required={false}
231+
validator={validateEnvVar}
232+
defaultValues={build_variables}
233+
/>
234+
</div>
235+
</CollapsibleSection>
236+
237+
<CollapsibleSection
238+
title={t('selfHost.configuration.sections.commands')}
239+
icon={Terminal}
240+
badge="Optional"
241+
description="Pre and post deployment scripts"
242+
defaultOpen={false}
243+
>
244+
<div className="grid sm:grid-cols-2 gap-4">
245+
<FormInputField
246+
form={form}
247+
label={t('selfHost.configuration.fields.preRunCommands.label')}
248+
name="pre_run_command"
249+
placeholder={t('selfHost.configuration.fields.preRunCommands.placeholder')}
250+
required={false}
251+
/>
252+
<FormInputField
253+
form={form}
254+
label={t('selfHost.configuration.fields.postRunCommands.label')}
255+
name="post_run_command"
256+
placeholder={t('selfHost.configuration.fields.postRunCommands.placeholder')}
257+
required={false}
258+
/>
259+
</div>
260+
</CollapsibleSection>
180261
</>
181262
)}
182263

183-
<div className="grid sm:grid-cols-2 gap-4">
184-
{renderReadOnlyField(
185-
t('selfHost.configuration.fields.environment.label'),
186-
environment,
187-
t('selfHost.configuration.fields.environment.description')
188-
)}
189-
{renderReadOnlyField(
190-
t('selfHost.configuration.fields.branch.label'),
191-
branch,
192-
t('selfHost.configuration.fields.branch.description')
193-
)}
194-
</div>
264+
<CollapsibleSection
265+
title={t('selfHost.configuration.sections.deploymentInformation')}
266+
icon={InfoIcon}
267+
badge="Read-only"
268+
description="Current deployment settings and metadata"
269+
defaultOpen={false}
270+
>
271+
<div className="grid sm:grid-cols-2 gap-4">
272+
{renderReadOnlyField(
273+
t('selfHost.configuration.fields.environment.label'),
274+
environment,
275+
t('selfHost.configuration.fields.environment.description')
276+
)}
277+
{renderReadOnlyField(
278+
t('selfHost.configuration.fields.branch.label'),
279+
branch,
280+
t('selfHost.configuration.fields.branch.description')
281+
)}
282+
</div>
283+
<div className="grid sm:grid-cols-2 gap-4">
284+
{renderReadOnlyField(
285+
t('selfHost.configuration.fields.domain.label'),
286+
domain,
287+
t('selfHost.configuration.fields.domain.description')
288+
)}
289+
{renderReadOnlyField(
290+
t('selfHost.configuration.fields.buildPack.label'),
291+
build_pack,
292+
t('selfHost.configuration.fields.buildPack.description')
293+
)}
294+
</div>
295+
</CollapsibleSection>
195296

196-
<div className="grid sm:grid-cols-2 gap-4">
197-
{renderReadOnlyField(
198-
t('selfHost.configuration.fields.domain.label'),
199-
domain,
200-
t('selfHost.configuration.fields.domain.description')
201-
)}
202-
{renderReadOnlyField(
203-
t('selfHost.configuration.fields.buildPack.label'),
204-
build_pack,
205-
t('selfHost.configuration.fields.buildPack.description')
206-
)}
297+
<div className="pt-4 flex justify-end">
298+
<Button type="submit" className="w-fit cursor-pointer" disabled={isLoading}>
299+
{isLoading
300+
? t('selfHost.configuration.buttons.updating')
301+
: t('selfHost.configuration.buttons.update')}
302+
</Button>
207303
</div>
208-
209-
<Button type="submit" className="w-full cursor-pointer" disabled={isLoading}>
210-
{isLoading
211-
? t('selfHost.configuration.buttons.updating')
212-
: t('selfHost.configuration.buttons.update')}
213-
</Button>
214304
</form>
215305
</Form>
216306
</AnyPermissionGuard>

view/lib/i18n/locales/en.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -918,6 +918,13 @@
918918
}
919919
},
920920
"configuration": {
921+
"sections": {
922+
"basicConfiguration": "Basic Configuration",
923+
"dockerConfiguration": "Docker Configuration",
924+
"environmentVariables": "Environment Variables",
925+
"commands": "Commands",
926+
"deploymentInformation": "Deployment Information"
927+
},
921928
"fields": {
922929
"applicationName": {
923930
"label": "Application Name",

view/lib/i18n/locales/es.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -895,6 +895,13 @@
895895
}
896896
},
897897
"configuration": {
898+
"sections": {
899+
"basicConfiguration": "Configuración Básica",
900+
"dockerConfiguration": "Configuración Docker",
901+
"environmentVariables": "Variables de Entorno",
902+
"commands": "Comandos",
903+
"deploymentInformation": "Información de Despliegue"
904+
},
898905
"fields": {
899906
"applicationName": {
900907
"label": "Nombre de la Aplicación",

view/lib/i18n/locales/fr.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,13 @@
899899
}
900900
},
901901
"configuration": {
902+
"sections": {
903+
"basicConfiguration": "Configuration de Base",
904+
"dockerConfiguration": "Configuration Docker",
905+
"environmentVariables": "Variables d'Environnement",
906+
"commands": "Commandes",
907+
"deploymentInformation": "Informations de Déploiement"
908+
},
902909
"fields": {
903910
"applicationName": {
904911
"label": "Nom de l'Application",

view/lib/i18n/locales/kn.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -895,6 +895,13 @@
895895
}
896896
},
897897
"configuration": {
898+
"sections": {
899+
"basicConfiguration": "ಮೂಲಭೂತ ಕಾನ್ಫಿಗರೇಶನ್",
900+
"dockerConfiguration": "ಡಾಕರ್ ಕಾನ್ಫಿಗರೇಶನ್",
901+
"environmentVariables": "ಪರಿಸರ ವೇರಿಯೇಬಲ್‌ಗಳು",
902+
"commands": "ಆಜ್ಞೆಗಳು",
903+
"deploymentInformation": "ನಿಯೋಜನೆ ಮಾಹಿತಿ"
904+
},
898905
"fields": {
899906
"applicationName": {
900907
"label": "ಅಪ್ಲಿಕೇಶನ್ ಹೆಸರು",

0 commit comments

Comments
 (0)