@@ -4,12 +4,14 @@ import { Button } from '@/components/ui/button';
44import { Form } from '@/components/ui/form' ;
55import FormInputField from '@/components/ui/form-input-field' ;
66import { FormSelectTagInputField } from '@/components/ui/form-select-tag-field' ;
7+ import { Collapsible , CollapsibleContent , CollapsibleTrigger } from '@/components/ui/collapsible' ;
78import { BuildPack , Environment } from '@/redux/types/deploy-form' ;
89import useUpdateDeployment from '../../hooks/use_update_deployment' ;
910import { parsePort } from '../../utils/parsePort' ;
1011import { useTranslation } from '@/hooks/use-translation' ;
1112import { ResourceGuard , AnyPermissionGuard } from '@/components/rbac/PermissionGuard' ;
1213import { Skeleton } from '@/components/ui/skeleton' ;
14+ import { ChevronDownIcon , ChevronRightIcon , SettingsIcon , ServerIcon , CodeIcon , InfoIcon , Terminal } from 'lucide-react' ;
1315
1416interface 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+
3188export 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 >
0 commit comments