1414 * limitations under the License.
1515 */
1616
17- import { Icon } from '@iconify/react' ;
17+ import { Icon , InlineIcon } from '@iconify/react' ;
1818import {
1919 Autocomplete ,
2020 Box ,
2121 Button ,
2222 CircularProgress ,
2323 DialogActions ,
2424 DialogContent ,
25+ FormControl ,
2526 Grid ,
26- Input ,
27+ Tab ,
28+ Tabs ,
2729 TextField ,
30+ Tooltip ,
2831 Typography ,
2932} from '@mui/material' ;
33+ import { styled } from '@mui/system' ;
3034import { loadAll } from 'js-yaml' ;
3135import { Dispatch , FormEvent , SetStateAction , useState } from 'react' ;
36+ import { useDropzone } from 'react-dropzone' ;
3237import { Trans , useTranslation } from 'react-i18next' ;
3338import { Redirect , useHistory } from 'react-router' ;
3439import { useClustersConf } from '../../lib/k8s' ;
@@ -38,7 +43,26 @@ import { createRouteURL } from '../../lib/router';
3843import { ViewYaml } from '../advancedSearch/ResourceSearch' ;
3944import Table from '../common/Table' ;
4045import { KubeIcon } from '../resourceMap/kubeIcon/KubeIcon' ;
41- import { toKubernetesName } from './projectUtils' ;
46+ import { PROJECT_ID_LABEL , toKubernetesName } from './projectUtils' ;
47+
48+ const DropZoneBox = styled ( Box ) ( {
49+ border : 1 ,
50+ borderRadius : 1 ,
51+ borderWidth : 2 ,
52+ borderColor : 'rgba(0, 0, 0)' ,
53+ borderStyle : 'dashed' ,
54+ padding : '20px' ,
55+ margin : '20px' ,
56+ display : 'flex' ,
57+ flexDirection : 'column' ,
58+ alignItems : 'center' ,
59+ '&:hover' : {
60+ borderColor : 'rgba(0, 0, 0, 0.5)' ,
61+ } ,
62+ '&:focus-within' : {
63+ borderColor : 'rgba(0, 0, 0, 0.5)' ,
64+ } ,
65+ } ) ;
4266
4367async function createProjectFromYaml ( {
4468 items,
@@ -63,7 +87,7 @@ async function createProjectFromYaml({
6387 metadata : {
6488 name : k8sName ,
6589 labels : {
66- PROJECT_ID_LABEL : k8sName ,
90+ [ PROJECT_ID_LABEL ] : k8sName ,
6791 } ,
6892 } as any ,
6993 } as any ;
@@ -117,6 +141,96 @@ export function CreateNew() {
117141
118142 const [ errors , setErrors ] = useState < Record < string , string > > ( { } ) ;
119143
144+ // New state for URL and tab management
145+ const [ currentTab , setCurrentTab ] = useState ( 0 ) ;
146+ const [ yamlUrl , setYamlUrl ] = useState ( '' ) ;
147+ const [ isLoadingFromUrl , setIsLoadingFromUrl ] = useState ( false ) ;
148+
149+ // Function to load YAML from URL
150+ const loadFromUrl = async ( ) => {
151+ if ( ! yamlUrl . trim ( ) ) {
152+ setErrors ( prev => ( { ...prev , url : t ( 'URL is required' ) } ) ) ;
153+ return ;
154+ }
155+
156+ setIsLoadingFromUrl ( true ) ;
157+ setErrors ( prev => ( { ...prev , url : '' } ) ) ;
158+
159+ try {
160+ const response = await fetch ( yamlUrl ) ;
161+ if ( ! response . ok ) {
162+ throw new Error ( `HTTP ${ response . status } : ${ response . statusText } ` ) ;
163+ }
164+ const content = await response . text ( ) ;
165+ const docs = loadAll ( content ) as KubeObjectInterface [ ] ;
166+ const validDocs = docs . filter ( doc => ! ! doc ) ;
167+ setItems ( validDocs ) ;
168+ setErrors ( prev => ( { ...prev , items : '' } ) ) ;
169+ } catch ( error ) {
170+ setErrors ( prev => ( {
171+ ...prev ,
172+ url : t ( 'Failed to load from URL: {{error}}' , {
173+ error : ( error as Error ) . message ,
174+ } ) ,
175+ } ) ) ;
176+ } finally {
177+ setIsLoadingFromUrl ( false ) ;
178+ }
179+ } ;
180+
181+ // File drop functionality
182+ const onDrop = ( acceptedFiles : File [ ] ) => {
183+ setErrors ( prev => ( { ...prev , items : '' } ) ) ;
184+
185+ const promises = acceptedFiles . map ( file => {
186+ return new Promise < { docs : KubeObjectInterface [ ] } > ( ( resolve , reject ) => {
187+ const reader = new FileReader ( ) ;
188+ reader . onload = ( ) => {
189+ const content = reader . result as string ;
190+ try {
191+ const docs = loadAll ( content ) as KubeObjectInterface [ ] ;
192+ const validDocs = docs . filter ( doc => ! ! doc ) ;
193+ resolve ( { docs : validDocs } ) ;
194+ } catch ( err ) {
195+ console . error ( 'Error parsing YAML file:' , file . name , err ) ;
196+ // Resolve with empty array for failed files
197+ resolve ( { docs : [ ] } ) ;
198+ }
199+ } ;
200+ reader . onerror = err => {
201+ console . error ( 'Error reading file:' , file . name , err ) ;
202+ reject ( err ) ;
203+ } ;
204+ reader . readAsText ( file ) ;
205+ } ) ;
206+ } ) ;
207+
208+ Promise . all ( promises )
209+ . then ( results => {
210+ const newDocs = results . flatMap ( result => result . docs ) ;
211+ setItems ( prevItems => [ ...prevItems , ...newDocs ] ) ;
212+ } )
213+ . catch ( err => {
214+ console . error ( 'An error occurred while processing files.' , err ) ;
215+ setErrors ( prev => ( {
216+ ...prev ,
217+ items : t ( 'Error processing files: {{error}}' , {
218+ error : ( err as Error ) . message ,
219+ } ) ,
220+ } ) ) ;
221+ } ) ;
222+ } ;
223+
224+ const { getRootProps, getInputProps, open } = useDropzone ( {
225+ onDrop,
226+ accept : {
227+ 'application/x-yaml' : [ '.yaml' , '.yml' ] ,
228+ 'text/yaml' : [ '.yaml' , '.yml' ] ,
229+ 'text/plain' : [ '.yaml' , '.yml' ] ,
230+ } ,
231+ multiple : true ,
232+ } ) ;
233+
120234 const handleCreate = async ( e : FormEvent ) => {
121235 e . preventDefault ( ) ;
122236
@@ -131,7 +245,6 @@ export function CreateNew() {
131245 errors . items = t ( 'No resources have been uploaded' ) ;
132246 }
133247
134- console . log ( errors ) ;
135248 if ( Object . keys ( errors ) . length > 0 ) {
136249 setErrors ( errors ) ;
137250 return ;
@@ -219,81 +332,109 @@ export function CreateNew() {
219332 </ Grid >
220333
221334 < Grid item xs = { 3 } >
222- < Typography > { t ( 'Upload files(s) ' ) } </ Typography >
335+ < Typography > { t ( 'Load resources ' ) } </ Typography >
223336 < Typography variant = "body2" color = "text.secondary" sx = { { mb : 1 } } >
224- { t ( 'Upload your YAML file(s) ' ) }
337+ { t ( 'Upload files or load from URL ' ) }
225338 </ Typography >
226339 </ Grid >
227340 < Grid item xs = { 9 } >
228341 { errors . items && < Typography color = "error" > { errors . items } </ Typography > }
229342
230- < Input
231- type = "file"
232- error = { ! ! errors . items }
233- sx = { theme => ( {
234- '::before,::after' : {
235- display : 'none' ,
236- } ,
237- p : 1 ,
238- background : theme . palette . background . muted ,
239- border : '1px solid' ,
240- borderColor : theme . palette . divider ,
241- borderRadius : theme . shape . borderRadius + 'px' ,
242- mt : 0 ,
243- } ) }
244- inputProps = { {
245- accept : '.yaml,.yml,applicaiton/yaml' ,
246- multiple : true ,
247- } }
248- onChange = { e => {
249- const fileList = ( e . target as HTMLInputElement ) . files ;
250- if ( ! fileList ) return ;
251-
252- const promises = Array . from ( fileList ) . map ( file => {
253- return new Promise < { docs : KubeObjectInterface [ ] } > ( ( resolve , reject ) => {
254- const reader = new FileReader ( ) ;
255- reader . onload = ( ) => {
256- const content = reader . result as string ;
257- try {
258- const docs = loadAll ( content ) as KubeObjectInterface [ ] ;
259- resolve ( { docs } ) ;
260- } catch ( err ) {
261- console . error ( 'Error parsing YAML file:' , file . name , err ) ;
262- // Optionally, you can decide how to handle parsing errors.
263- // Here we resolve with an empty array for the failed file.
264- resolve ( { docs : [ ] } ) ;
343+ < Box sx = { { width : '100%' } } >
344+ < Box sx = { { borderBottom : 1 , borderColor : 'divider' } } >
345+ < Tabs value = { currentTab } onChange = { ( _ , newValue ) => setCurrentTab ( newValue ) } >
346+ < Tab label = { t ( 'Upload Files' ) } />
347+ < Tab label = { t ( 'Load from URL' ) } />
348+ </ Tabs >
349+ </ Box >
350+
351+ { /* File Upload Tab */ }
352+ { currentTab === 0 && (
353+ < Box sx = { { pt : 2 } } >
354+ < DropZoneBox border = { 1 } borderColor = "secondary.main" { ...getRootProps ( ) } >
355+ < FormControl >
356+ < input { ...getInputProps ( ) } />
357+ < Tooltip
358+ title = { t ( 'Drag & drop YAML files here or click to choose files' ) }
359+ placement = "top"
360+ >
361+ < Button
362+ variant = "contained"
363+ onClick = { open }
364+ startIcon = { < InlineIcon icon = "mdi:upload" width = { 24 } /> }
365+ >
366+ { t ( 'Choose Files' ) }
367+ </ Button >
368+ </ Tooltip >
369+ </ FormControl >
370+ < Typography variant = "body2" color = "text.secondary" sx = { { mt : 1 } } >
371+ { t ( 'Supports .yaml and .yml files' ) }
372+ </ Typography >
373+ </ DropZoneBox >
374+ </ Box >
375+ ) }
376+
377+ { /* URL Loading Tab */ }
378+ { currentTab === 1 && (
379+ < Box sx = { { pt : 2 } } >
380+ < Box sx = { { display : 'flex' , gap : 2 , alignItems : 'flex-start' } } >
381+ < TextField
382+ fullWidth
383+ label = { t ( 'YAML URL' ) }
384+ placeholder = { t ( 'Enter URL to YAML file' ) }
385+ variant = "outlined"
386+ size = "small"
387+ value = { yamlUrl }
388+ onChange = { e => setYamlUrl ( e . target . value ) }
389+ error = { ! ! errors . url }
390+ helperText = { errors . url }
391+ disabled = { isLoadingFromUrl }
392+ />
393+ < Button
394+ variant = "contained"
395+ onClick = { loadFromUrl }
396+ disabled = { isLoadingFromUrl || ! yamlUrl . trim ( ) }
397+ startIcon = {
398+ isLoadingFromUrl ? (
399+ < CircularProgress size = { 16 } />
400+ ) : (
401+ < InlineIcon icon = "mdi:download" width = { 24 } />
402+ )
265403 }
266- } ;
267- reader . onerror = err => {
268- console . error ( 'Error reading file:' , file . name , err ) ;
269- reject ( err ) ;
270- } ;
271- reader . readAsText ( file ) ;
272- } ) ;
273- } ) ;
274-
275- Promise . all ( promises )
276- . then ( results => {
277- const newDocs = results . flatMap ( result => result . docs ) ;
278- setItems ( newDocs ) ;
279- // setYamlDocs(currentDocs => [...currentDocs, ...newDocs]);
280- } )
281- . catch ( err => {
282- console . error ( 'An error occurred while processing files.' , err ) ;
283- } ) ;
284- } }
285- />
404+ >
405+ { isLoadingFromUrl ? t ( 'Loading...' ) : t ( 'Load' ) }
406+ </ Button >
407+ </ Box >
408+ < Typography variant = "body2" color = "text.secondary" sx = { { mt : 1 } } >
409+ { t ( 'Load YAML resources from a remote URL' ) }
410+ </ Typography >
411+ </ Box >
412+ ) }
413+ </ Box >
286414 </ Grid >
287415 </ Grid >
288416
289417 { items . length > 0 && (
290418 < Box sx = { { display : 'flex' , flexDirection : 'column' , gap : 3 , mt : 3 } } >
291- < Typography > { t ( 'Items' ) } </ Typography >
419+ < Box
420+ sx = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' } }
421+ >
422+ < Typography >
423+ { t ( 'Loaded Resources ({{count}})' , { count : items . length } ) }
424+ </ Typography >
425+ < Button
426+ variant = "outlined"
427+ size = "small"
428+ onClick = { ( ) => setItems ( [ ] ) }
429+ startIcon = { < InlineIcon icon = "mdi:delete" width = { 16 } /> }
430+ >
431+ { t ( 'Clear All' ) }
432+ </ Button >
433+ </ Box >
292434 < Box
293435 sx = { {
294436 display : 'flex' ,
295437 flexDirection : 'column' ,
296- marginTop : items . length > 0 ? '-46px' : 0 ,
297438 } }
298439 >
299440 < Table
@@ -318,6 +459,11 @@ export function CreateNew() {
318459 header : t ( 'Name' ) ,
319460 accessorFn : item => item . metadata . name ,
320461 } ,
462+ {
463+ id : 'apiVersion' ,
464+ header : t ( 'API Version' ) ,
465+ accessorFn : item => item . apiVersion ,
466+ } ,
321467 {
322468 id : 'actions' ,
323469 header : t ( 'Actions' ) ,
@@ -357,8 +503,11 @@ export function CreateNew() {
357503 { t ( 'Creating following resources in this project:' ) }
358504 </ Typography >
359505 < Box sx = { { display : 'flex' , flexDirection : 'column' , gap : 1 , mt : 1 } } >
360- { creationState . createdResources . map ( resource => (
361- < Box sx = { { display : 'flex' , alignItems : 'center' , gap : 1 } } >
506+ { creationState . createdResources . map ( ( resource , index ) => (
507+ < Box
508+ key = { `created-${ resource . kind } -${ resource . metadata . name } -${ index } ` }
509+ sx = { { display : 'flex' , alignItems : 'center' , gap : 1 } }
510+ >
362511 < KubeIcon kind = { resource . kind as any } width = "24px" height = "24px" />
363512 < Box > { resource . metadata . name } </ Box >
364513 < Box sx = { theme => ( { color : theme . palette . success . main } ) } >
0 commit comments