1- import React , { useEffect , useState } from "react" ;
2- import { Alert , Form , Input , Modal , Select , Spin , Typography } from "antd" ;
1+ import React , { useEffect , useRef , useState } from "react" ;
2+ import { Alert , Button , Form , Input , Modal , Select , Spin , Typography } from "antd" ;
33import { useTranslation } from "react-i18next" ;
44import * as HelmBackend from "./backend/HelmBackend" ;
55import * as NamespaceBackend from "./backend/NamespaceBackend" ;
@@ -12,12 +12,17 @@ export default function HelmInstallModal({open, chart, onClose, onInstalled}) {
1212 const [ namespaces , setNamespaces ] = useState ( [ ] ) ;
1313 const [ valuesYAML , setValuesYAML ] = useState ( "" ) ;
1414 const [ valuesLoading , setValuesLoading ] = useState ( false ) ;
15- const [ submitting , setSubmitting ] = useState ( false ) ;
15+ const [ installing , setInstalling ] = useState ( false ) ;
16+ const [ done , setDone ] = useState ( false ) ;
1617 const [ error , setError ] = useState ( null ) ;
18+ const [ logs , setLogs ] = useState ( [ ] ) ;
19+ const logEndRef = useRef ( null ) ;
1720
1821 useEffect ( ( ) => {
1922 if ( ! open || ! chart ) { return ; }
2023 setError ( null ) ;
24+ setLogs ( [ ] ) ;
25+ setDone ( false ) ;
2126
2227 NamespaceBackend . getNamespaces ( ) . then ( res => {
2328 if ( res . status === "ok" ) {
@@ -47,38 +52,56 @@ export default function HelmInstallModal({open, chart, onClose, onInstalled}) {
4752 }
4853 } , [ open , chart , form ] ) ;
4954
55+ useEffect ( ( ) => {
56+ if ( logEndRef . current ) {
57+ logEndRef . current . scrollIntoView ( { behavior : "smooth" } ) ;
58+ }
59+ } , [ logs ] ) ;
60+
5061 const handleClose = ( ) => {
5162 form . resetFields ( ) ;
5263 setValuesYAML ( "" ) ;
5364 setError ( null ) ;
65+ setLogs ( [ ] ) ;
66+ setDone ( false ) ;
67+ setInstalling ( false ) ;
5468 onClose ( ) ;
5569 } ;
5670
5771 const handleOk = ( ) => {
72+ if ( done ) {
73+ onInstalled ?. ( ) ;
74+ handleClose ( ) ;
75+ return ;
76+ }
5877 form . validateFields ( ) . then ( values => {
59- setSubmitting ( true ) ;
78+ setInstalling ( true ) ;
6079 setError ( null ) ;
61- HelmBackend . installHelmChart ( {
62- releaseName : values . releaseName ,
63- namespace : values . namespace ,
64- chartName : chart . chartName ,
65- repoURL : chart . repoURL ,
66- version : values . version || chart . version ,
67- valuesYAML,
68- } ) . then ( res => {
69- if ( res . status === "ok" ) {
80+ setLogs ( [ ] ) ;
81+ HelmBackend . installHelmChartStream (
82+ {
83+ releaseName : values . releaseName ,
84+ namespace : values . namespace ,
85+ chartName : chart . chartName ,
86+ repoURL : chart . repoURL ,
87+ version : values . version || chart . version ,
88+ valuesYAML,
89+ } ,
90+ line => setLogs ( prev => [ ...prev , line ] )
91+ )
92+ . then ( ( ) => {
93+ setDone ( true ) ;
7094 onInstalled ?. ( ) ;
71- handleClose ( ) ;
72- } else {
73- setError ( res . msg ) ;
74- }
75- } ) . catch ( e => setError ( e . message ) ) . finally ( ( ) => setSubmitting ( false ) ) ;
95+ } )
96+ . catch ( e => setError ( e . message ) )
97+ . finally ( ( ) => setInstalling ( false ) ) ;
7698 } ) ;
7799 } ;
78100
79101 if ( ! chart ) { return null ; }
80102
81103 const nsOptions = namespaces . map ( ns => ( { label : ns . name , value : ns . name } ) ) ;
104+ const showLog = installing || done || error ;
82105
83106 return (
84107 < Modal
@@ -93,57 +116,101 @@ export default function HelmInstallModal({open, chart, onClose, onInstalled}) {
93116 </ span >
94117 }
95118 open = { open }
96- onOk = { handleOk }
97- onCancel = { handleClose }
98- okText = { t ( "helm:Install" ) }
99- confirmLoading = { submitting }
100- width = { 680 }
119+ onCancel = { installing ? undefined : handleClose }
120+ closable = { ! installing }
121+ maskClosable = { ! installing }
122+ footer = {
123+ < div style = { { display : "flex" , justifyContent : "flex-end" , gap : 8 } } >
124+ { ! installing && (
125+ < Button onClick = { handleClose } > { done ? t ( "general:Close" ) : t ( "general:Cancel" ) } </ Button >
126+ ) }
127+ { ! done && (
128+ < Button type = "primary" loading = { installing } onClick = { handleOk } >
129+ { t ( "helm:Install" ) }
130+ </ Button >
131+ ) }
132+ { done && (
133+ < Button type = "primary" onClick = { handleOk } >
134+ { t ( "general:Done" ) }
135+ </ Button >
136+ ) }
137+ </ div >
138+ }
139+ width = { 700 }
101140 destroyOnHidden
102141 >
103- { error && < Alert type = "error" message = { error } showIcon style = { { marginBottom : 16 } } closable onClose = { ( ) => setError ( null ) } /> }
142+ { error && (
143+ < Alert type = "error" message = { error } showIcon style = { { marginBottom : 16 } } closable onClose = { ( ) => setError ( null ) } />
144+ ) }
104145
105- < Form form = { form } layout = "vertical" >
106- < div style = { { display : "flex" , gap : 12 } } >
107- < Form . Item
108- style = { { flex : 1 } }
109- label = { t ( "helm:Release name" ) }
110- name = "releaseName"
111- rules = { [
112- { required : true } ,
113- { pattern : / ^ [ a - z 0 - 9 ] [ a - z 0 - 9 - ] * $ / , message : t ( "helm:Release name pattern" ) } ,
114- ] }
115- >
116- < Input />
117- </ Form . Item >
118- < Form . Item style = { { flex : 1 } } label = { t ( "general:Namespaces" ) } name = "namespace" rules = { [ { required : true } ] } >
119- < Select options = { nsOptions } showSearch />
120- </ Form . Item >
121- < Form . Item style = { { width : 130 } } label = { t ( "helm:Version" ) } name = "version" >
122- < Input placeholder = { chart . version ?? "latest" } />
146+ { ! showLog && (
147+ < Form form = { form } layout = "vertical" >
148+ < div style = { { display : "flex" , gap : 12 } } >
149+ < Form . Item
150+ style = { { flex : 1 } }
151+ label = { t ( "helm:Release name" ) }
152+ name = "releaseName"
153+ rules = { [
154+ { required : true } ,
155+ { pattern : / ^ [ a - z 0 - 9 ] [ a - z 0 - 9 - ] * $ / , message : t ( "helm:Release name pattern" ) } ,
156+ ] }
157+ >
158+ < Input />
159+ </ Form . Item >
160+ < Form . Item style = { { flex : 1 } } label = { t ( "general:Namespaces" ) } name = "namespace" rules = { [ { required : true } ] } >
161+ < Select options = { nsOptions } showSearch />
162+ </ Form . Item >
163+ < Form . Item style = { { width : 130 } } label = { t ( "helm:Version" ) } name = "version" >
164+ < Input placeholder = { chart . version ?? "latest" } />
165+ </ Form . Item >
166+ </ div >
167+
168+ < Form . Item label = { t ( "helm:Values (YAML)" ) } >
169+ { valuesLoading ? (
170+ < div style = { { textAlign : "center" , padding : 24 } } >
171+ < Spin size = "small" />
172+ < Text style = { { marginLeft : 8 , color : "rgba(0,0,0,0.45)" } } > { t ( "helm:Loading values" ) } </ Text >
173+ </ div >
174+ ) : (
175+ < textarea
176+ value = { valuesYAML }
177+ onChange = { e => setValuesYAML ( e . target . value ) }
178+ rows = { 14 }
179+ style = { {
180+ width : "100%" , fontFamily : "monospace" , fontSize : 12 ,
181+ padding : "8px 10px" , borderRadius : 6 ,
182+ border : "1px solid #d9d9d9" , resize : "vertical" , outline : "none" ,
183+ boxSizing : "border-box" ,
184+ } }
185+ spellCheck = { false }
186+ />
187+ ) }
123188 </ Form . Item >
124- </ div >
189+ </ Form >
190+ ) }
125191
126- < Form . Item label = { t ( "helm:Values (YAML)" ) } >
127- { valuesLoading ? (
128- < div style = { { textAlign : "center" , padding : 24 } } >
129- < Spin size = "small" /> < Text style = { { marginLeft : 8 , color : "rgba(0,0,0,0.45)" } } > { t ( "helm:Loading values" ) } </ Text >
130- </ div >
131- ) : (
132- < textarea
133- value = { valuesYAML }
134- onChange = { e => setValuesYAML ( e . target . value ) }
135- rows = { 14 }
136- style = { {
137- width : "100%" , fontFamily : "monospace" , fontSize : 12 ,
138- padding : "8px 10px" , borderRadius : 6 ,
139- border : "1px solid #d9d9d9" , resize : "vertical" , outline : "none" ,
140- boxSizing : "border-box" ,
141- } }
142- spellCheck = { false }
143- />
192+ { showLog && (
193+ < div
194+ style = { {
195+ background : "#1a1a1a" , borderRadius : 6 , padding : "10px 14px" ,
196+ fontFamily : "monospace" , fontSize : 12 , color : "#d4d4d4" ,
197+ height : 320 , overflowY : "auto" , lineHeight : 1.6 ,
198+ } }
199+ >
200+ { logs . length === 0 && installing && (
201+ < span style = { { color : "#888" } } >
202+ < Spin size = "small" style = { { marginRight : 8 } } />
203+ { t ( "helm:Installing" ) } ...
204+ </ span >
144205 ) }
145- </ Form . Item >
146- </ Form >
206+ { logs . map ( ( line , i ) => (
207+ < div key = { i } style = { { color : line . startsWith ( "ERROR" ) ? "#f87171" : done && i === logs . length - 1 ? "#4ade80" : "#d4d4d4" } } >
208+ { line }
209+ </ div >
210+ ) ) }
211+ < div ref = { logEndRef } />
212+ </ div >
213+ ) }
147214 </ Modal >
148215 ) ;
149216}
0 commit comments