import { compact , isNull , uniq } from 'lodash' ; import { useEffect , useRef , useState } from 'react' ; import { ImInsertTemplate } from 'react-icons/im' ; import { MdClose } from 'react-icons/md' ; import { useHistory } from 'react-router-dom' ; import API from '../../../api' ; import { ChartTemplate , ChartTmplTypeFile , ErrorKind , RepositoryKind , SearchFiltersURL } from '../../../types' ; import alertDispatcher from '../../../utils/alertDispatcher' ; import BlockCodeButtons from '../../common/BlockCodeButtons' ; import ErrorBoundary from '../../common/ErrorBoundary' ; import Loading from '../../common/Loading' ; import Modal from '../../common/Modal' ; import styles from './ChartTemplatesModal.module.css' ; import Template from './Template' ; import TemplatesList from './TemplatesList' ; interface Props { normalizedName : string ; packageId : string ; version : string ; repoKind : RepositoryKind ; visibleChartTemplates : boolean ; visibleTemplate ? : string ; searchUrlReferer ? : SearchFiltersURL ; fromStarredPage ? : boolean ; } interface FileProps { name : string ; extension : string ; } const KIND = /(\nkind: [A-Za-z0-9"]*|^kind: [A-Za-z0-9"]*)/g ; const getFileNameAndExt = ( str ) : FileProps => { const file = str . split ( '/' ) . pop ( ) || str ; return { name : file . substr ( 0 , file . lastIndexOf ( '.' ) ) , extension : file . substr ( file . lastIndexOf ( '.' ) + 1 , file . length ) , } ; } ; const getResourceKinds = ( data ) : string [ ] => { const kinds = data . match ( KIND ) ; if ( kinds ) { const cleanKinds = kinds . map ( ( kind ) => { const parts = kind . split ( ':' ) ; return parts [ 1 ] . replaceAll ( '"' , '' ) . trim ( ) ; } ) ; return uniq ( compact ( cleanKinds ) ) ; } return [ ] ; } ; const decodeData = ( data ) : string => { try { return atob ( data ) ; } catch { return Buffer . from ( data , 'base64' ) . toString ( 'binary' ) ; } } ; const formatTemplates = ( templates ) : ChartTemplate [ ] => { let finalTemplates = [ ] ; let finalHelpers = [ ] ; templates . forEach ( ( template ) => { const templateName = template . name . replace ( 'templates/' , '' ) ; const { name , extension } = getFileNameAndExt ( templateName ) ; if ( [ 'yaml' , 'tpl' ] . includes ( extension ) ) { const decodedData = decodeData ( template . data ) ; const tmpl = { name : templateName , fileName : name , resourceKinds : getResourceKinds ( decodedData ) , data : decodedData , } ; if ( extension === 'yaml' ) { finalTemplates . push ( { ... tmpl , type : ChartTmplTypeFile . Template } ) ; } else { finalHelpers . push ( { ... tmpl , type : ChartTmplTypeFile . Helper } ) ; } } } ) ; return [ ... finalTemplates , ... finalHelpers ] ; } ; const ChartTemplatesModal = ( props ) => { const history = useHistory ( ) ; const anchor = useRef < HTMLDivElement > ( null ) ; const [ openStatus , setOpenStatus ] = useState < boolean > ( false ) ; const [ templates , setTemplates ] = useState < ChartTemplate [ ] | null | undefined > ( ) ; const [ activeTemplate , setActiveTemplate ] = useState < ChartTemplate | null > ( null ) ; const [ values , setValues ] = useState < any | undefined > ( ) ; const [ isLoading , setIsLoading ] = useState < boolean > ( false ) ; const [ currentPkgId , setCurrentPkgId ] = useState < string > ( props . packageId ) ; const [ currentVersion , setCurrentVersion ] = useState < string > ( props . version ) ; const [ isChangingTemplate , setIsChangingTemplate ] = useState < boolean > ( false ) ; const onTemplateChange = ( template ) => { setIsChangingTemplate ( true ) ; setActiveTemplate ( template ) ; updateUrl ( template ? template . name : undefined ) ; if ( ! isNull ( template ) ) { if ( anchor && anchor . current ) { anchor . current . scrollIntoView ( { block : 'start' , inline : 'nearest' , behavior : 'smooth' , } ) ; } } } ; const updateUrl = ( templateName ? ) => { history . replace ( { search : `?modal=template${ templateName ? `&template=${ templateName }` : '' }` , state : { searchUrlReferer : props . searchUrlReferer , fromStarredPage : props . fromStarredPage } , } ) ; } ; const cleanUrl = ( ) => { history . replace ( { search : '' , state : { searchUrlReferer : props . searchUrlReferer , fromStarredPage : props . fromStarredPage } , } ) ; } ; useEffect ( ( ) => { if ( props . visibleChartTemplates ) { if ( ! openStatus && props . repoKind === RepositoryKind . Helm ) { onOpenModal ( ) ; } else { cleanUrl ( ) ; } } } , [ ] ) ; useEffect ( ( ) => { if ( props . packageId !== currentPkgId && openStatus ) { setOpenStatus ( false ) ; } } , [ props . packageId ] ) ; if ( props . repoKind !== RepositoryKind . Helm ) return null ; async function getChartTemplates ( ) { try { setIsLoading ( true ) ; setCurrentPkgId ( props . packageId ) ; setCurrentVersion ( props . version ) ; const data = await API . getChartTemplates ( props . packageId , props . version ) ; if ( data && data . templates ) { const formattedTemplates = formatTemplates ( data . templates ) ; if ( formattedTemplates . length > 0 ) { setTemplates ( formattedTemplates ) ; let activeTmpl ; if ( props . visibleTemplate ) { activeTmpl = formattedTemplates . find ( ( tmpl ) => tmpl . name === props . visibleTemplate ) ; if ( ! activeTmpl ) { updateUrl ( formattedTemplates [ 0 ] . name ) ; } } else { updateUrl ( formattedTemplates [ 0 ] . name ) ; } setActiveTemplate ( activeTmpl || formattedTemplates [ 0 ] ) ; setValues ( data ? { Values : { ... data . values } } : null ) ; setOpenStatus ( true ) ; } else { setTemplates ( null ) ; alertDispatcher . postAlert ( { type : 'warning' , message : 'This Helm chart does not contain any template.' , } ) ; cleanUrl ( ) ; } } else { setTemplates ( null ) ; alertDispatcher . postAlert ( { type : 'warning' , message : 'This Helm chart does not contain any template.' , } ) ; cleanUrl ( ) ; } setIsLoading ( false ) ; } catch ( err ) { if ( err . kind === ErrorKind . NotFound ) { alertDispatcher . postAlert ( { type : 'danger' , message : 'We could not find the templates for this chart version. Please check that the chart tgz package still exists in the source repository as it might not be available anymore.' , dismissOn : 10 * 1000 , } ) ; } else { alertDispatcher . postAlert ( { type : 'danger' , message : 'An error occurred getting chart templates, please try again later.' , } ) ; } setTemplates ( null ) ; setValues ( null ) ; cleanUrl ( ) ; setIsLoading ( false ) ; } } const onOpenModal = ( ) => { if ( templates && props . packageId === currentPkgId && props . version === currentVersion ) { setOpenStatus ( true ) ; updateUrl ( templates [ 0 ] . name ) ; } else { getChartTemplates ( ) ; } } ; const onCloseModal = ( ) => { setOpenStatus ( false ) ; setActiveTemplate ( templates ? templates [ 0 ] : null ) ; history . replace ( { search : '' , state : { searchUrlReferer : props . searchUrlReferer , fromStarredPage : props . fromStarredPage } , } ) ; } ; return ( < div className = "mb-2" > < div className = "text-center" > < button className = "btn btn-outline-secondary btn-sm text-nowrap w-100" onClick = { onOpenModal } aria - label = "Open templates modal" > < div className = "d-flex flex-row align-items-center justify-content-center" > { isLoading ? ( < > < span className = "spinner-grow spinner-grow-sm" role = "status" aria - hidden = "true" / > < span className = "d-none d-md-inline ms-2 fw-bold" > Loading templates ... < /span> < />) : (<> < ImInsertTemplate / > < span className = "ms-2 fw-bold text-uppercase" > Templates < /span> < />)} < /div> < /button> < /div> { openStatus && templates && ( < Modal modalDialogClassName = { styles . modalDialog } modalClassName = "h-100" header = { < div className = { `h3 m-2 flex-grow-1 ${ styles . title }` } > Templates < /div>} onClose={onCloseModal} closeButton={<div className="w-100 d-flex flex-row align-items-center justify-content-between"> < small className = "me-3" > < span className = "fw-bold" > TIP : < /span> some extra info may be displayed when hovering over{' '} < span className = "fw-bold" > values < /span> entries and other{' '} < span className = "fw-bold" > built - in objects and functions < /span>. < /small> < button className = "btn btn-sm btn-outline-secondary text-uppercase" onClick = { ( ) => setOpenStatus ( false ) } aria - label = "Close" > < div className = "d-flex flex-row align-items-center" > < MdClose className = "me-2" / > < div > Close < /div> < /div> < /button> < /div>} open={openStatus} breakPoint="md"> < div className = "h-100 mw-100" > < div className = "d-flex flex-row align-items-strecht g-0 h-100 mh-100" > < div className = "col-3 h-100" > < TemplatesList templates = { templates } activeTemplateName = { activeTemplate ? activeTemplate . name : undefined } onTemplateChange = { onTemplateChange } / > < /div> < div className = "col-9 ps-3 h-100" > < div className = { `position-relative h-100 mh-100 border ${ styles . templateWrapper }` } > { isChangingTemplate && activeTemplate && < Loading />} { activeTemplate && ( < BlockCodeButtons filename = { `${ props . normalizedName }-${ activeTemplate . name }` } content = { activeTemplate . data } boxShadowColor = "var(--extra-light-gray)" / > ) } < pre className = { `text-muted h-100 mh-100 mb-0 overflow-auto position-relative ${ styles . pre }` } > { activeTemplate && ( < ErrorBoundary className = { styles . errorAlert } message = "Something went wrong rendering the template." > < > < div className = { `position-absolute ${ styles . anchor }` } ref = { anchor } / > < Template template = { activeTemplate ! } values = { values } setIsChangingTemplate = { setIsChangingTemplate } / > < /> < /ErrorBoundary>)} < /pre> < /div> < /div> < /div> < /div> < /Modal>)} < /div>); } ; export default ChartTemplatesModal ;