import classnames from 'classnames' ; import { isUndefined } from 'lodash' ; import { useEffect , useLayoutEffect , useRef , useState } from 'react' ; import { VscListTree } from 'react-icons/vsc' ; import { useHistory } from 'react-router-dom' ; import SyntaxHighlighter from 'react-syntax-highlighter' ; import { tomorrowNight } from 'react-syntax-highlighter/dist/cjs/styles/hljs' ; import { isDocument , isMap , isSeq , LineCounter , parseDocument } from 'yaml' ; import API from '../../../api' ; import useOutsideClick from '../../../hooks/useOutsideClick' ; import { ErrorKind , SearchFiltersURL } from '../../../types' ; import alertDispatcher from '../../../utils/alertDispatcher' ; import getJMESPathForValuesSchema from '../../../utils/getJMESPathForValuesSchema' ; import BlockCodeButtons from '../../common/BlockCodeButtons' ; import ButtonCopyToClipboard from '../../common/ButtonCopyToClipboard' ; import Modal from '../../common/Modal' ; import ValuesSearch from '../../common/ValuesSearch' ; import styles from './Values.module.css' ; interface Props { packageId : string ; version : string ; normalizedName : string ; visibleValues : boolean ; visibleValuesPath ? : string ; searchUrlReferer ? : SearchFiltersURL ; fromStarredPage ? : boolean ; } interface Lines { [ key ] : string ; } const getPathsPerLine = ( values ) : Lines => { const lineCounter = new LineCounter ( ) ; const doc = parseDocument ( values , { lineCounter } ) ; let lines = { } ; const extractKeys = ( elem , path ? ) => { if ( elem ) { if ( isDocument ( elem ) && elem . contents ) { extractKeys ( elem . contents ) ; } else if ( isMap ( elem ) ) { elem . items . forEach ( ( item ) => { const currentPath = getJMESPathForValuesSchema ( item . key . value , path ) ; const line = lineCounter . linePos ( item . key . range [ 0 ] ) . line ; lines [ line ] = currentPath ; extractKeys ( item . value , currentPath ) ; } ) ; } else if ( isSeq ( elem ) ) { elem . items . forEach ( ( item , index ) => { extractKeys ( item , `${ path }[${ index }]` ) ; } ) ; } } } ; extractKeys ( doc ) ; return lines ; } ; const Values = ( props ) => { const history = useHistory ( ) ; const code = useRef < HTMLDivElement | null > ( null ) ; const [ openStatus , setOpenStatus ] = useState < boolean > ( false ) ; const [ values , setValues ] = useState < string | undefined | null > ( ) ; const [ currentPkgId , setCurrentPkgId ] = useState < string | undefined > ( undefined ) ; const [ isLoading , setIsLoading ] = useState < boolean > ( false ) ; const [ activeLine , setActiveLine ] = useState < string | undefined > ( ) ; const [ lines , setLines ] = useState < Lines | undefined > ( ) ; const [ clickedLine , setClickedLine ] = useState < number | undefined > ( ) ; const [ topPositionMenu , setTopPositionMenu ] = useState < number | undefined > ( ) ; const [ arrowLeftMargin , setArrowLeftMargin ] = useState < number | undefined > ( ) ; const [ fullWidth , setFullWidth ] = useState < number | undefined > ( ) ; const cleanClickedLine = ( ) => { setClickedLine ( undefined ) ; setTopPositionMenu ( undefined ) ; setArrowLeftMargin ( undefined ) ; } ; useOutsideClick ( [ code ] , ! isUndefined ( clickedLine ) , cleanClickedLine ) ; const updateUrl = ( lineNumber ? ) => { let selectedPath ; if ( ! isUndefined ( lineNumber ) && ! isUndefined ( lines ) ) { selectedPath = lines [ parseInt ( lineNumber ) ] ; } history . replace ( { search : `?modal=values${ selectedPath ? `&path=${ selectedPath }` : '' }` , state : { searchUrlReferer : props . searchUrlReferer , fromStarredPage : props . fromStarredPage } , } ) ; } ; async function getValues ( ) { try { setIsLoading ( true ) ; const data = await API . getChartValues ( props . packageId , props . version ) ; setValues ( data ) ; setLines ( getPathsPerLine ( data ) ) ; setCurrentPkgId ( props . packageId ) ; setIsLoading ( false ) ; setOpenStatus ( true ) ; } catch ( err ) { if ( err . kind === ErrorKind . NotFound ) { alertDispatcher . postAlert ( { type : 'danger' , message : 'We could not find the default values 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 default values, please try again later.' , } ) ; } setValues ( null ) ; setIsLoading ( false ) ; } } const onOpenModal = ( ) => { getValues ( ) ; history . replace ( { search : `?modal=values${ props . visibleValuesPath ? `&path=${ props . visibleValuesPath }` : '' }` , state : { searchUrlReferer : props . searchUrlReferer , fromStarredPage : props . fromStarredPage } , } ) ; } ; const onCloseModal = ( ) => { setValues ( undefined ) ; setOpenStatus ( false ) ; setActiveLine ( undefined ) ; cleanClickedLine ( ) ; history . replace ( { search : '' , state : { searchUrlReferer : props . searchUrlReferer , fromStarredPage : props . fromStarredPage } , } ) ; } ; useEffect ( ( ) => { if ( props . visibleValues && ! openStatus && isUndefined ( currentPkgId ) ) { onOpenModal ( ) ; } } , [ ] ) ; useEffect ( ( ) => { if ( ( openStatus || props . visibleValues ) && ! isUndefined ( currentPkgId ) ) { onCloseModal ( ) ; } } , [ props . packageId ] ) ; const onSearch = ( selectedLine ? ) => { setActiveLine ( selectedLine ) ; updateUrl ( selectedLine ) ; } ; useEffect ( ( ) => { const scrollIntoView = ( path ) => { if ( isUndefined ( path ) || path === '' ) return ; try { const element = document . getElementById ( `line_${ path }` ) ; if ( element && code && code . current ) { code . current . scrollTo ( { top : element . offsetTop , left : 0 , behavior : 'smooth' , } ) ; } } finally { return ; } } ; if ( ! isUndefined ( activeLine ) ) { scrollIntoView ( activeLine ) ; } } , [ activeLine ] ) ; useLayoutEffect ( ( ) => { if ( isUndefined ( activeLine ) && ! isUndefined ( props . visibleValuesPath ) && ! isUndefined ( lines ) ) { const selectedLine = Object . keys ( lines ) . find ( ( line ) => lines [ parseInt ( line ) ] === props . visibleValuesPath ) ; if ( selectedLine ) { setTimeout ( ( ) => { setActiveLine ( selectedLine ) ; } , 100 ) ; } else { history . replace ( { search : '?modal=values' , state : { searchUrlReferer : props . searchUrlReferer , fromStarredPage : props . fromStarredPage } , } ) ; } } } , [ props . visibleValuesPath , lines ] ) ; useEffect ( ( ) => { if ( ! isUndefined ( clickedLine ) ) { if ( code && code . current && isUndefined ( fullWidth ) ) { setFullWidth ( code . current . scrollWidth ) ; } const element = document . getElementById ( `line_${ clickedLine }` ) ; if ( element ) { const attr = element . querySelector ( '.hljs-attr' ) ; if ( attr ) { const lineBoundingX = element . getBoundingClientRect ( ) . x ; const attrBounding = attr . getBoundingClientRect ( ) ; setArrowLeftMargin ( attrBounding . width / 2.0 + attrBounding . x - lineBoundingX - 6 ) ; } setTopPositionMenu ( element . offsetTop + element . offsetHeight + 5 ) ; } } else { setTopPositionMenu ( undefined ) ; } } , [ clickedLine , fullWidth ] ) ; return ( < > < button className = "btn btn-outline-secondary btn-sm w-100" onClick = { onOpenModal } aria - label = "Open default values modal" > < div className = "d-flex flex-row align-items-center justify-content-center text-uppercase" > { isLoading ? ( < > < span className = "spinner-grow spinner-grow-sm" role = "status" aria - hidden = "true" / > < span className = "ms-2 fw-bold" > Getting values ... < /span> < />) : (<> < VscListTree className = "me-2" / > < span className = "fw-bold" > Default values < /span> < />)} < /div> < /button> { openStatus && values && ( < Modal modalDialogClassName = { styles . modalDialog } modalClassName = "h-100" header = { < div className = { `h3 m-2 flex-grow-1 ${ styles . title }` } > Default values < /div>} onClose={onCloseModal} open={openStatus} breakPoint="md"> < div className = "mw-100 h-100 d-flex flex-column overflow-hidden" > { lines && ( < ValuesSearch pathsObj = { lines } activePath = { activeLine } onSearch = { onSearch } wrapperClassName = "w-50" / > ) } { values && ( < div className = { `position-relative flex-grow-1 ${ styles . content }` } > < BlockCodeButtons filename = { `values-${ props . normalizedName }.yaml` } content = { values } tooltipType = "light" / > < div ref = { code } className = { `overflow-auto h-100 position-relative ${ styles . codeWrapper }` } > { ! isUndefined ( lines ) && ! isUndefined ( clickedLine ) && ! isUndefined ( topPositionMenu ) && ( < div role = "complementary" className = { classnames ( 'dropdown-menu dropdown-menu-left show' , styles . dropdown ) } style = { { top : topPositionMenu } } > < div className = { `dropdown-arrow ${ styles . arrow }` } style = { { left : arrowLeftMargin ? `${ arrowLeftMargin }px` : '1rem' } } /> < ButtonCopyToClipboard className = "dropdown-item mw-100" text = { lines [ clickedLine ] } contentBtn = "Copy entry path to clipboard" visibleBtnText label = "Copy entry path to clipboard" onClick = { ( ) => { setClickedLine ( undefined ) ; setTopPositionMenu ( undefined ) ; } } noTooltip / > < /div>)} < SyntaxHighlighter language = "yaml" style = { tomorrowNight } customStyle = { { backgroundColor : 'transparent' , padding : '1.5rem' , paddingLeft : '4.5rem' , lineHeight : '1.25rem' , marginBottom : '0' , height : '100%' , fontSize : '80%' , color : '#f8f9fa' , overflow : 'visible' , } } lineNumberStyle = { { display : 'none' , } } className = "customYAML" useInlineStyles = { false } showLineNumbers wrapLines lineProps = { ( lineNumber ) => { return { id : `line_${ lineNumber }` , className : 'line' , style : { position : 'relative' , width : fullWidth ? `${ fullWidth }px` : 'auto' } , 'data-line-number' : lineNumber , 'data-clickable-line' : lines && lines [ lineNumber ] ? 'true' : 'false' , 'data-active-line' : lineNumber === clickedLine , onClick ( ) { const isClicked = clickedLine === lineNumber ; if ( lines && ! isUndefined ( lines [ lineNumber ] ) && ! isClicked ) { setClickedLine ( lineNumber ) ; } else { cleanClickedLine ( ) ; } } , } ; } } > { values } < /SyntaxHighlighter> < /div> < /div>)} < /div> < /Modal>)} < />); } ; export default Values ;