import React from 'react' ; import { load , dump } from 'js-yaml' ; import { Tab } from '@kui-shell/core' ; import { Button , Icons } from '@kui-shell/plugin-client-common' ; import { Caption , TableComposable , Tbody , Tr , Td } from '@patternfly/react-table' ; import { ActionGroup , Form , FormGroup , FormSelect as Select , FormSelectOption as SelectItem , TextInput } from '@patternfly/react-core' ; import '../../src/web/scss/static/metrics.scss' ; import { ErrorDisplay } from './error-react' ; import deleteMetric from '../components/delete-metric' ; import restoreMetric from '../components/restore-metric' ; import { getMetricConfig , removeExtraneousMetaData , ITER8_METRIC_NAMES } from '../components/metric-config' ; import { CounterMetric , CounterMetrics , RatioMetric , RatioMetrics , MetricConfigMap } from '../components/metric-config-types' ; import { kubectlApplyRule } from '../components/traffic-split' ; let COUNTER_METRIC_REQUIRED_ATTRIBUTES ; let RATIO_METRIC_REQUIRED_ATTRIBUTES ; export enum FormTypes { add = 'add' , edit = 'edit' } export enum MetricTypes { counter = 'counter' , ratio = 'ratio' } enum AttributeTypes { 'input' , 'dropdown' } type MetricAttributesData = AttributeData [ ] ; type AttributeData = InputAttributeData | DropdownAttributeData ; type InputAttributeData = { name : string ; printableName : string ; description : string ; type : AttributeTypes . input ; required : boolean ; invalidChecks ? : { [ key in FormTypes ] ? : InvalidCheck ; } ; } ; type DropdownAttributeData = { name : string ; printableName : string ; description : string ; type : AttributeTypes . dropdown ; required : boolean ; dropdownOptions : DropdownOptions ; /**
     * The option that will be selected on render. If not set, then the first
     * element of dropdown options will be selected.
     */ noDefaultDropdownOptionPlaceholder ? : string ; invalidChecks ? : { [ key in FormTypes ] ? : InvalidCheck ; } ; } ; type InvalidCheck = { invalidText : string ; check : ( value : any , state : any ) => boolean ; } ; type DropdownOptions = { value : any ; printableOption : string ; } [ ] ; const ratioDefaultDropdownOption = 'Please select a counter metric' ; const ratioDescription = 'Counter metric ID used to calculate ratio metric' ; const noneSelected = 'None selected' ; /**
 * A list of all attributes that can be used to created a counter metric
 *
 * Easily extendable to add new attributes
 */ const COUNTER_METRIC_ATTRIBUTES_DATA = [ { name : 'name' , printableName : 'Name' , description : 'Unique identifier for counter metric' , type : AttributeTypes . input , required : true , invalidChecks : { add : { invalidText : 'Another counter metric already has this name' , check : ( value , state ) => { return state . counterMetricNames . includes ( value ) ; } } } } , { name : 'query_template' , printableName : 'Query template' , description : 'Prometheus query used to populate counter metric' , type : AttributeTypes . input , required : true } , { name : 'preferred_direction' , printableName : 'Preferred direction' , description : 'Whether a higher or lower counter metric is preferred. Default: lower' , type : AttributeTypes . dropdown , required : true , dropdownOptions : [ { printableOption : 'Lower' , value : 'lower' } , { printableOption : 'Higher' , value : 'higher' } ] } , { name : 'units' , printableName : 'Units' , description : 'Optional: units of the counter metric' , type : AttributeTypes . input , required : false } ] ; /**
 * A list of all attributes that can be used to created a ratio metric
 *
 * Easily extendable to add new attributes
 */ const RATIO_METRIC_ATTRIBUTES_DATA = [ { name : 'name' , printableName : 'Name' , description : 'Unique identifier for ratio metric' , type : AttributeTypes . input , required : true , invalidChecks : { add : { invalidText : 'Another ratio metric already has this name' , check : ( value , state ) => { return state . ratioMetricNames . includes ( value ) ; } } } } , { name : 'numerator' , printableName : 'Numerator' , description : ratioDescription , type : AttributeTypes . dropdown , required : true , dropdownOptions : [ ] , noDefaultDropdownOptionPlaceholder : ratioDefaultDropdownOption } , { name : 'denominator' , printableName : 'Denominator' , description : ratioDescription , type : AttributeTypes . dropdown , required : true , dropdownOptions : [ ] , noDefaultDropdownOptionPlaceholder : ratioDefaultDropdownOption } , { name : 'preferred_direction' , printableName : 'Preferred direction' , description : 'Whether a higher or lower ratio metric is preferred. Default: lower' , type : AttributeTypes . dropdown , required : true , dropdownOptions : [ { printableOption : 'Lower' , value : 'lower' } , { printableOption : 'Higher' , value : 'higher' } ] } , { name : 'zero_to_one' , printableName : 'Zero to one' , description : 'Optional: whether the ratio metric is between 0 and 1, inclusive' , type : AttributeTypes . dropdown , required : false , dropdownOptions : [ { printableOption : 'True' , value : true } , { printableOption : 'False' , value : false } ] , noDefaultDropdownOptionPlaceholder : noneSelected } ] ; /**
 * Given some metric type, get all default values
 */ function getDefaultConfig ( metricAttributesData ) { const result = { } ; Object . values ( metricAttributesData ) . forEach ( attribute => { if ( attribute . type === AttributeTypes . input ) { result [ attribute . name ] = '' ; } else { /**
             * If there is no default dropdown option placeholder, then that means
             * the default option should be the first entry among the dropdown
             * options
             */ if ( ! attribute . noDefaultDropdownOptionPlaceholder ) { if ( attribute . dropdownOptions . length > 0 ) { result [ attribute . name ] = attribute . dropdownOptions [ 0 ] . value ; } else { throw new Error ( `No known dropdown options for attribute '${ attribute . name }'` ) ; } } } } ) ; return result ; } const DEFAULT_COUNTER_METRIC_CONFIG = getDefaultConfig ( COUNTER_METRIC_ATTRIBUTES_DATA ) ; const DEFAULT_RATIO_METRIC_CONFIG = getDefaultConfig ( RATIO_METRIC_ATTRIBUTES_DATA ) ; enum DisplayMode { 'getMetrics' , 'addMetric' , 'editMetric' , 'error' } type MetricDetailsState = { configMap : MetricConfigMap ; rawConfigMap : any ; counterMetrics : CounterMetrics ; ratioMetrics : RatioMetrics ; counterMetricNames : string [ ] ; ratioMetricNames : string [ ] ; counterMetricsState : MetricsState ; ratioMetricsState : MetricsState ; display : DisplayMode ; selectedMetricName ? : string ; selectedType ? : MetricTypes ; newMetric ? : Partial < CounterMetric | RatioMetric > ; formSubmitted ? : boolean ; errorString ? : string ; } ; /**
 * Create a simple DropdownOptions from a string array
 *
 * The keys and the values of the elements of DropdownOptions are from the array
 */ function createBasicStringDropdownOptions ( values ) { const rs = [ ] ; values . forEach ( value => { rs . push ( { printableOption : value , value } ) ; } ) ; return rs ; } type Props = Tab ; export default class MetricDetailsMode extends React . Component < Props , MetricDetailsState > { private args : Props ; public constructor ( props ) { super ( props ) ; this . args = props ; try { this . state = { configMap : null , rawConfigMap : { } , counterMetrics : [ ] , ratioMetrics : [ ] , counterMetricNames : [ ] , ratioMetricNames : [ ] , counterMetricsState : { } , ratioMetricsState : { } , display : DisplayMode . getMetrics , formSubmitted : false , errorString : 'None' } ; this . getConfigMap ( ) ; } catch ( error ) { this . state = { configMap : undefined , rawConfigMap : undefined , counterMetrics : undefined , ratioMetrics : undefined , counterMetricNames : undefined , ratioMetricNames : undefined , counterMetricsState : undefined , ratioMetricsState : undefined , formSubmitted : undefined , display : DisplayMode . getMetrics , errorString : error } ; } } private getConfigMap ( ) { setTimeout ( async ( ) => { const [ rawConfigMap ] = await Promise . all ( [ getMetricConfig ( this . args ) ] ) ; this . setState ( { rawConfigMap : rawConfigMap } ) ; this . constructorHelper ( ) ; } ) ; } private constructorHelper = ( ) => { try { const counterMetrics = load ( this . state . rawConfigMap . data [ 'counter_metrics.yaml' ] ) as CounterMetrics ; const ratioMetrics = load ( this . state . rawConfigMap . data [ 'ratio_metrics.yaml' ] ) as RatioMetrics ; const cleanConfigMap = removeExtraneousMetaData ( this . state . rawConfigMap ) ; const counterMetricNames = counterMetrics . map ( counterMetric => { return counterMetric . name ; } ) ; const ratioMetricNames = ratioMetrics . map ( ratioMetric => { return ratioMetric . name ; } ) ; const counterMetricDropdownOptions = createBasicStringDropdownOptions ( counterMetricNames ) ; const numeratorAttribute = RATIO_METRIC_ATTRIBUTES_DATA . find ( attribute => { return attribute . name === 'numerator' ; } ) ; const denominatorAttribute = RATIO_METRIC_ATTRIBUTES_DATA . find ( attribute => { return attribute . name === 'denominator' ; } ) ; ( numeratorAttribute as DropdownAttributeData ) . dropdownOptions = counterMetricDropdownOptions ; ( denominatorAttribute as DropdownAttributeData ) . dropdownOptions = counterMetricDropdownOptions ; COUNTER_METRIC_REQUIRED_ATTRIBUTES = COUNTER_METRIC_ATTRIBUTES_DATA . filter ( attribute => { return attribute . required ; } ) . map ( attribute => { return attribute . name ; } ) ; RATIO_METRIC_REQUIRED_ATTRIBUTES = RATIO_METRIC_ATTRIBUTES_DATA . filter ( attribute => { return attribute . required ; } ) . map ( attribute => { return attribute . name ; } ) ; const { counterMetricsState , ratioMetricsState } = this . generateMetricsStates ( counterMetrics , ratioMetrics ) ; this . setState ( { configMap : cleanConfigMap , counterMetrics : counterMetrics , ratioMetrics : ratioMetrics , counterMetricNames : counterMetricNames , ratioMetricNames : ratioMetricNames , ratioMetricsState : ratioMetricsState , counterMetricsState : counterMetricsState } ) ; } catch ( error ) { console . log ( error ) ; throw new Error ( 'Could not obtain metric config' ) ; } } ; private refreshState = ( ) => { console . log ( 'Refreshed state' ) ; try { this . setState ( { configMap : null , rawConfigMap : { } , counterMetrics : [ ] , ratioMetrics : [ ] , counterMetricNames : [ ] , ratioMetricNames : [ ] , counterMetricsState : { } , ratioMetricsState : { } , display : DisplayMode . getMetrics , formSubmitted : false , errorString : 'None' } ) ; this . getConfigMap ( ) ; } catch ( error ) { this . setState ( { configMap : undefined , counterMetrics : undefined , ratioMetrics : undefined , counterMetricNames : undefined , ratioMetricNames : undefined , counterMetricsState : undefined , ratioMetricsState : undefined , formSubmitted : undefined , display : DisplayMode . getMetrics , errorString : error } ) ; } } ; private generateMetricsStates = ( counterMetrics , ratioMetrics ) : { counterMetricsState : MetricsState ; ratioMetricsState : MetricsState ; } => { const counterMetricsState = { } ; const ratioMetricsState = { } ; counterMetrics . forEach ( metric => { counterMetricsState [ metric . name ] = { name : metric . name , isDeleted : false , alsoDelete : [ ] , alsoRestore : [ ] , details : metric , custom : ! ITER8_METRIC_NAMES . counter . includes ( metric . name ) } ; } ) ; ratioMetrics . forEach ( metric => { ratioMetricsState [ metric . name ] = { name : metric . name , isDeleted : false , alsoDelete : [ ] , alsoRestore : [ metric . numerator , metric . denominator ] , details : metric , custom : ! ITER8_METRIC_NAMES . ratio . includes ( metric . name ) } ; try { counterMetricsState [ metric . numerator ] . alsoDelete . push ( metric . name ) ; } catch ( e ) { throw new Error ( `Ratio metric '${ metric . name }' has numerator metric
          '${ metric . numerator }' which is not defined.` ) ; } try { counterMetricsState [ metric . denominator ] . alsoDelete . push ( metric . name ) ; } catch ( e ) { throw new Error ( `Ratio metric '${ metric . name }' has denominator metric
          '${ metric . denominator }' which is not defined.` ) ; } } ) ; return { counterMetricsState , ratioMetricsState } ; } ; private getInitialRowsFromObjectKeys = ( object ) => { return Object . keys ( object ) . map ( propertyName => { return { id : propertyName , isExpanded : false } ; } ) ; } ; private renderIter8ImmutableMetricWarning = ( mode ) => { return < div className = "warningtext" > Warning : Cannot { mode } iter8 metrics < /div>; } ; private renderDeleteWarning = ( metricName , type ) => { return ( < div > < div className = "deletedtext" > Deleted < /div> < div className = "clickableicon" onClick = { ( ) => this . restoreMetricHandler ( metricName , type ) } > < Icons icon = "Retry" / > { type === MetricTypes . ratio && this . state . ratioMetricsState [ metricName ] . custom && this . state . ratioMetricsState [ metricName ] . alsoRestore . length ? ( < div className = "warningtext" > Warning : Will also restore { this . state . ratioMetricsState [ metricName ] . alsoRestore . join ( ', ' ) } < /div>) : null} < /div> < /div>); } ; private displayGetMetrics = ( ) => { this . setState ( { display : DisplayMode . getMetrics , selectedMetricName : undefined , selectedType : undefined , newMetric : undefined , formSubmitted : undefined } ) ; } ; private displayAddMetric = ( type ) => { const defaultConfig = type === MetricTypes . counter ? DEFAULT_COUNTER_METRIC_CONFIG : DEFAULT_RATIO_METRIC_CONFIG ; this . setState ( { display : DisplayMode . addMetric , selectedType : type , newMetric : defaultConfig , formSubmitted : false } ) ; } ; private displayEditMetric = ( metricName , type ) => { const { counterMetrics , ratioMetrics } = this . state ; const selectedMetric = type === MetricTypes . counter ? counterMetrics . find ( counterMetric => { return counterMetric . name === metricName ; } ) : ratioMetrics . find ( ratioMetric => { return ratioMetric . name === metricName ; } ) ; const selectedMetricCopy = JSON . parse ( JSON . stringify ( selectedMetric ) ) ; const defaultConfig = type === MetricTypes . counter ? DEFAULT_COUNTER_METRIC_CONFIG : DEFAULT_RATIO_METRIC_CONFIG ; const editedMetric = { ... defaultConfig , ... selectedMetricCopy } ; this . setState ( { display : DisplayMode . editMetric , selectedMetricName : metricName , selectedType : type , newMetric : editedMetric , formSubmitted : false } ) ; } ; private addMetricHandler = e => { console . log ( 'click submitted' ) ; this . setState ( { formSubmitted : true } ) ; const { configMap , counterMetrics , ratioMetrics , selectedType , newMetric } = this . state ; if ( selectedType === MetricTypes . counter ) { console . log ( 'new counter metric config:' , newMetric ) ; /**
             * Ensure that there are no name collisions
             *
             * This is separate from the invalid check in the form
             */ const uniqueName = ! counterMetrics . some ( metric => { return metric . name === newMetric . name ; } ) ; const allRequiredFieldsPresent = COUNTER_METRIC_REQUIRED_ATTRIBUTES . every ( id => { return newMetric [ id ] ; } ) ; if ( allRequiredFieldsPresent && uniqueName ) { const cleanNewMetric = { } ; Object . entries ( newMetric ) . forEach ( ( [ attributeName , attributeValue ] ) => { const attribute = COUNTER_METRIC_ATTRIBUTES_DATA . find ( attribute => attribute . name === attributeName ) ; if ( attribute . type === AttributeTypes . input ) { if ( ( attributeValue as string ) . length > 0 ) { cleanNewMetric [ attributeName ] = attributeValue ; } } else { cleanNewMetric [ attributeName ] = attributeValue ; } } ) ; /**
                 * TS ignore because writing with proper typing would result in lots of
                 * code duplication and greatly enhance complexity.
                 *
                 * This should be of the correct data type and safe to push at this point
                 * with the final checks.
                 */ counterMetrics . push ( cleanNewMetric as CounterMetric ) ; const stringifiedMetrics = dump ( counterMetrics ) ; configMap . data [ 'counter_metrics.yaml' ] = stringifiedMetrics ; kubectlApplyRule ( configMap , this . args ) ; const counterMetricNames = counterMetrics . map ( counterMetric => { return counterMetric . name ; } ) ; const counterMetricDropdownOptions = createBasicStringDropdownOptions ( counterMetricNames ) ; const numeratorAttribute = RATIO_METRIC_ATTRIBUTES_DATA . find ( attribute => { return attribute . name === 'numerator' ; } ) ; const denominatorAttribute = RATIO_METRIC_ATTRIBUTES_DATA . find ( attribute => { return attribute . name === 'denominator' ; } ) ; ( numeratorAttribute as DropdownAttributeData ) . dropdownOptions = counterMetricDropdownOptions ; ( denominatorAttribute as DropdownAttributeData ) . dropdownOptions = counterMetricDropdownOptions ; const { counterMetricsState } = this . generateMetricsStates ( counterMetrics , ratioMetrics ) ; this . setState ( { configMap , counterMetrics , display : DisplayMode . getMetrics , counterMetricsState : counterMetricsState , selectedType : undefined , newMetric : undefined , formSubmitted : undefined } ) ; } else { console . log ( 'uniqueName:' , uniqueName ) ; console . log ( 'allRequiredFieldsPresent:' , allRequiredFieldsPresent ) ; } } else { console . log ( 'new ratio metric config:' , newMetric ) ; /**
             * Ensure that there are no name collisions
             *
             * This is separate from the invalid check in the form
             */ const uniqueName = ! ratioMetrics . some ( metric => { return metric . name === newMetric . name ; } ) ; const allRequiredFieldsPresent = RATIO_METRIC_REQUIRED_ATTRIBUTES . every ( id => { return newMetric [ id ] ; } ) ; if ( allRequiredFieldsPresent && uniqueName ) { const cleanNewMetric = { } ; Object . entries ( newMetric ) . forEach ( ( [ attributeName , attributeValue ] ) => { const attribute = RATIO_METRIC_ATTRIBUTES_DATA . find ( attribute => attribute . name === attributeName ) ; if ( attribute . type === AttributeTypes . input ) { if ( ( attributeValue as string ) . length > 0 ) { cleanNewMetric [ attributeName ] = attributeValue ; } } else { cleanNewMetric [ attributeName ] = attributeValue ; } } ) ; /**
                 * TS ignore because writing with proper typing would result in lots of
                 * code duplication and greatly enhance complexity.
                 *
                 * This should be of the correct data type and safe to push at this point
                 * with the final checks.
                 */ ratioMetrics . push ( cleanNewMetric as RatioMetric ) ; const stringifiedMetrics = dump ( ratioMetrics ) ; configMap . data [ 'ratio_metrics.yaml' ] = stringifiedMetrics ; kubectlApplyRule ( configMap , this . args ) ; const counterMetricNames = counterMetrics . map ( counterMetric => { return counterMetric . name ; } ) ; const counterMetricDropdownOptions = createBasicStringDropdownOptions ( counterMetricNames ) ; const numeratorAttribute = RATIO_METRIC_ATTRIBUTES_DATA . find ( attribute => { return attribute . name === 'numerator' ; } ) ; const denominatorAttribute = RATIO_METRIC_ATTRIBUTES_DATA . find ( attribute => { return attribute . name === 'denominator' ; } ) ; ( numeratorAttribute as DropdownAttributeData ) . dropdownOptions = counterMetricDropdownOptions ; ( denominatorAttribute as DropdownAttributeData ) . dropdownOptions = counterMetricDropdownOptions ; const { ratioMetricsState } = this . generateMetricsStates ( counterMetrics , ratioMetrics ) ; this . setState ( { configMap , ratioMetrics , display : DisplayMode . getMetrics , ratioMetricsState : ratioMetricsState , selectedType : undefined , newMetric : undefined , formSubmitted : undefined } ) ; } else { console . log ( 'uniqueName:' , uniqueName ) ; console . log ( 'allRequiredFieldsPresent:' , allRequiredFieldsPresent ) ; } } e . preventDefault ( ) ; } ; private editMetricHandler = e => { console . log ( 'click submitted' ) ; this . setState ( { formSubmitted : true } ) ; const { selectedType } = this . state ; let { configMap , counterMetrics , ratioMetrics , selectedMetricName , newMetric } = this . state ; if ( selectedType === MetricTypes . counter ) { console . log ( 'edited counter metric config:' , newMetric ) ; /**
             * Ensure that there are no name collisions
             *
             * This is separate from the invalid check in the form
             */ const uniqueName = ! counterMetrics . filter ( metric => { return metric . name !== selectedMetricName ; } ) . some ( metric => { return metric . name === newMetric . name ; } ) ; const allRequiredFieldsPresent = COUNTER_METRIC_REQUIRED_ATTRIBUTES . every ( id => { return newMetric [ id ] ; } ) ; if ( allRequiredFieldsPresent && uniqueName ) { const cleanNewMetric = { } ; Object . entries ( newMetric ) . forEach ( ( [ attributeName , attributeValue ] ) => { const attribute = COUNTER_METRIC_ATTRIBUTES_DATA . find ( attribute => attribute . name === attributeName ) ; if ( attribute . type === AttributeTypes . input ) { if ( ( attributeValue as string ) . length > 0 ) { cleanNewMetric [ attributeName ] = attributeValue ; } } else { cleanNewMetric [ attributeName ] = attributeValue ; } } ) ; counterMetrics = counterMetrics . filter ( metric => { return metric . name !== selectedMetricName ; } ) ; counterMetrics . push ( cleanNewMetric as CounterMetric ) ; ratioMetrics . forEach ( metric => { if ( metric . numerator === selectedMetricName ) { metric . numerator = newMetric . name ; } if ( metric . denominator === selectedMetricName ) { metric . denominator = newMetric . name ; } } ) ; const stringifiedCounterMetrics = dump ( counterMetrics ) ; const stringifiedRatioMetrics = dump ( ratioMetrics ) ; configMap . data [ 'counter_metrics.yaml' ] = stringifiedCounterMetrics ; configMap . data [ 'ratio_metrics.yaml' ] = stringifiedRatioMetrics ; kubectlApplyRule ( configMap , this . args ) ; const counterMetricNames = counterMetrics . map ( counterMetric => { return counterMetric . name ; } ) ; const counterMetricDropdownOptions = createBasicStringDropdownOptions ( counterMetricNames ) ; const numeratorAttribute = RATIO_METRIC_ATTRIBUTES_DATA . find ( attribute => { return attribute . name === 'numerator' ; } ) ; const denominatorAttribute = RATIO_METRIC_ATTRIBUTES_DATA . find ( attribute => { return attribute . name === 'denominator' ; } ) ; ( numeratorAttribute as DropdownAttributeData ) . dropdownOptions = counterMetricDropdownOptions ; ( denominatorAttribute as DropdownAttributeData ) . dropdownOptions = counterMetricDropdownOptions ; const { counterMetricsState } = this . generateMetricsStates ( counterMetrics , ratioMetrics ) ; this . setState ( { configMap , counterMetrics , display : DisplayMode . getMetrics , counterMetricsState : counterMetricsState , selectedMetricName : undefined , selectedType : undefined , newMetric : undefined , formSubmitted : undefined } ) ; } else { console . log ( 'uniqueName:' , uniqueName ) ; console . log ( 'allRequiredFieldsPresent:' , allRequiredFieldsPresent ) ; } } else { console . log ( 'edited ratio metric config:' , newMetric ) ; /**
             * Ensure that there are no name collisions
             *
             * This is separate from the invalid check in the form
             */ const uniqueName = ! ratioMetrics . filter ( metric => { return metric . name !== selectedMetricName ; } ) . some ( metric => { return metric . name === newMetric . name ; } ) ; const allRequiredFieldsPresent = RATIO_METRIC_REQUIRED_ATTRIBUTES . every ( id => { return newMetric [ id ] ; } ) ; if ( allRequiredFieldsPresent && uniqueName ) { const cleanNewMetric = { } ; Object . entries ( newMetric ) . forEach ( ( [ attributeName , attributeValue ] ) => { const attribute = RATIO_METRIC_ATTRIBUTES_DATA . find ( attribute => attribute . name === attributeName ) ; if ( attribute . type === AttributeTypes . input ) { if ( ( attributeValue as string ) . length > 0 ) { cleanNewMetric [ attributeName ] = attributeValue ; } } else { cleanNewMetric [ attributeName ] = attributeValue ; } } ) ; ratioMetrics = ratioMetrics . filter ( metric => { return metric . name !== selectedMetricName ; } ) ; ratioMetrics . push ( cleanNewMetric as RatioMetric ) ; const stringifiedMetrics = dump ( ratioMetrics ) ; configMap . data [ 'ratio_metrics.yaml' ] = stringifiedMetrics ; kubectlApplyRule ( configMap , this . args ) ; const counterMetricNames = counterMetrics . map ( counterMetric => { return counterMetric . name ; } ) ; const counterMetricDropdownOptions = createBasicStringDropdownOptions ( counterMetricNames ) ; const numeratorAttribute = RATIO_METRIC_ATTRIBUTES_DATA . find ( attribute => { return attribute . name === 'numerator' ; } ) ; const denominatorAttribute = RATIO_METRIC_ATTRIBUTES_DATA . find ( attribute => { return attribute . name === 'denominator' ; } ) ; ( numeratorAttribute as DropdownAttributeData ) . dropdownOptions = counterMetricDropdownOptions ; ( denominatorAttribute as DropdownAttributeData ) . dropdownOptions = counterMetricDropdownOptions ; const { ratioMetricsState } = this . generateMetricsStates ( counterMetrics , ratioMetrics ) ; this . setState ( { configMap , ratioMetrics , display : DisplayMode . getMetrics , ratioMetricsState : ratioMetricsState , selectedMetricName : undefined , selectedType : undefined , newMetric : undefined , formSubmitted : undefined } ) ; } else { console . log ( 'uniqueName:' , uniqueName ) ; console . log ( 'allRequiredFieldsPresent:' , allRequiredFieldsPresent ) ; } } e . preventDefault ( ) ; } ; private deleteMetricHandler = ( metricName , type ) : void => { const newState = { ... this . state } ; if ( type === MetricTypes . counter ) { if ( deleteMetric ( this . state . rawConfigMap , metricName , type , this . args ) ) { newState . counterMetricsState [ metricName ] . isDeleted = true ; this . state . counterMetricsState [ metricName ] . alsoDelete . forEach ( ratioMetricName => { if ( deleteMetric ( this . state . rawConfigMap , ratioMetricName , MetricTypes . ratio , this . args ) ) { newState . ratioMetricsState [ ratioMetricName ] . isDeleted = true ; } } ) ; } } else { if ( deleteMetric ( this . state . rawConfigMap , metricName , type , this . args ) ) { newState . ratioMetricsState [ metricName ] . isDeleted = true ; } } this . setState ( newState ) ; } ; private restoreMetricHandler = ( metricName , type ) => { const { counterMetricsState , ratioMetricsState } = this . state ; const newState = { ... this . state } ; if ( type === MetricTypes . counter ) { if ( restoreMetric ( this . state . rawConfigMap , counterMetricsState [ metricName ] . details , type , this . args ) ) { newState . counterMetricsState [ metricName ] . isDeleted = false ; } } else { if ( restoreMetric ( this . state . rawConfigMap , ratioMetricsState [ metricName ] . details , type , this . args ) ) { newState . ratioMetricsState [ metricName ] . isDeleted = false ; ratioMetricsState [ metricName ] . alsoRestore . forEach ( counterMetricName => { if ( counterMetricsState [ counterMetricName ] . isDeleted ) { if ( restoreMetric ( this . state . rawConfigMap , counterMetricsState [ counterMetricName ] . details , MetricTypes . counter , this . args ) ) { newState . counterMetricsState [ counterMetricName ] . isDeleted = false ; } } } ) ; } } this . setState ( newState ) ; } ; private updateAttribute = ( e , attribute ) => { const editedAttributeValue = attribute . type === AttributeTypes . input ? e . target . value : attribute . dropdownOptions . find ( option => { return option . printableOption === e . target . value ; } ) . value ; const tempMetric = { } ; tempMetric [ attribute . name ] = editedAttributeValue ; this . setState ( { newMetric : { ... this . state . newMetric , ... tempMetric } } ) ; } ; private renderTableTitle = ( title , type ) => { return ( < div > { title } < Icons icon = "Add" onClick = { ( ) => this . displayAddMetric ( type ) } className = "clickableicon" / > < /div>); } ; private renderMetricDetails = ( metric , type ) => { const rows = this . getInitialRowsFromObjectKeys ( metric ) ; return ( < TableComposable className = "kui--table-like-wrapper" > < Tbody className = "innertable" > { rows . map ( row => { const attributeData = type === MetricTypes . counter ? COUNTER_METRIC_ATTRIBUTES_DATA . find ( attribute => { return attribute . name === row . id ; } ) : RATIO_METRIC_ATTRIBUTES_DATA . find ( attribute => { return attribute . name === row . id ; } ) ; if ( attributeData . type === AttributeTypes . dropdown ) { const dropdownOption = attributeData . dropdownOptions . find ( option => { return option . value === metric [ row . id ] ; } ) ; return ( < Tr key = { row . id } > < Td > { row . id } < /Td> < Td > { dropdownOption . printableOption } < /Td> < /Tr>); } return ( < Tr key = { row . id } > < Td > { row . id } < /Td> < Td > { metric [ row . id ] } < /Td> < /Tr>); } ) } < /Tbody> < /TableComposable>); } ; private renderMetricTable = ( metrics , type , title ) => { const rows = this . getInitialRowsFromObjectKeys ( metrics ) ; return ( < TableComposable className = "kui--table-like-wrapper" > < Caption > { this . renderTableTitle ( title , type ) } < /Caption> < Tbody isExpanded > { rows . map ( row => ( < React . Fragment key = { row . id } > < Tr aria - label = "Information Unavailable" isExpanded > < Td > { row . id } < /Td> < Td > < div className = "clickableicon" onClick = { ( ) => { if ( metrics [ row . id ] . custom ) { this . displayEditMetric ( row . id , type ) ; } } } > < Icons icon = "Edit" / > { ! metrics [ row . id ] . custom ? this . renderIter8ImmutableMetricWarning ( 'edit' ) : null } < /div> < /Td> < Td className = "width20" > < div > { metrics [ row . id ] . isDeleted ? ( < div > { this . renderDeleteWarning ( row . id , type ) } < /div>) : (<div className="clickableicon" onClick={() => { if ( metrics [ row . id ] . custom ) { this . deleteMetricHandler ( row . id , type ) ; } } } > < Icons icon = "Trash" / > { type === MetricTypes . counter && metrics [ row . id ] . custom && metrics [ row . id ] . alsoDelete . length > 0 ? ( < div className = "warningtext" > Warning : Will also delete { metrics [ row . id ] . alsoDelete . join ( ', ' ) } < /div>) : null} { ! metrics [ row . id ] . custom ? this . renderIter8ImmutableMetricWarning ( 'delete' ) : null } < /div>)} < /div> < /Td> < /Tr> { row . isExpanded && ( < Tr colSpan = { 4 } isExpanded > < div > { this . renderMetricDetails ( metrics [ row . id ] . details , type ) } < /div> < /Tr>)} < /React.Fragment>))} < /Tbody> < /TableComposable>); } ; private renderAttributeForm = ( attribute , formType ) => { const { newMetric , formSubmitted } = this . state ; if ( attribute . type === AttributeTypes . input ) { const invalid = ! newMetric [ attribute . name ] && formSubmitted && attribute . required ? true : attribute . invalidChecks && attribute . invalidChecks [ formType ] && attribute . invalidChecks [ formType ] . check ? attribute . invalidChecks [ formType ] . check ( newMetric [ attribute . name ] , this . state ) : false ; const invalidText = ! newMetric [ attribute . name ] ? 'This is a required attribute' : attribute . invalidChecks && attribute . invalidChecks [ formType ] && attribute . invalidChecks [ formType ] . invalidText ? attribute . invalidChecks [ formType ] . invalidText : '' ; const id = attribute . name ; const label = attribute . printableName ; return ( < FormGroup key = { attribute . name } label = { label } fieldId = { `get-metrics-${ label }-${ id }` } helperText = { attribute . description } helperTextInvalid = { invalidText } validated = { invalid ? 'error' : 'default' } isRequired = { attribute . required } > < TextInput id = { id } value = { newMetric [ attribute . name ] } onChange = { e => { return this . updateAttribute ( e , attribute ) ; } } > < /TextInput> < /FormGroup>); } else { const dropdownOption = attribute . dropdownOptions . find ( option => { return option . value === newMetric [ attribute . name ] ; } ) ; const printableValue = dropdownOption ? dropdownOption . printableOption : newMetric [ attribute . name ] ; const invalid = ! newMetric [ attribute . name ] && formSubmitted && attribute . required ? true : attribute . invalidChecks && attribute . invalidChecks [ formType ] && attribute . invalidChecks [ formType ] . check ? attribute . invalidChecks [ formType ] . check ( newMetric [ attribute . name ] , this . state ) : false ; const invalidText = ! newMetric [ attribute . name ] ? 'This is a required attribute. An option must be selected.' : attribute . invalidChecks && attribute . invalidChecks [ formType ] && attribute . invalidChecks [ formType ] . invalidText ? attribute . invalidChecks [ formType ] . invalidText : '' ; const id = attribute . name ; const label = attribute . printableName ; return ( < FormGroup key = { attribute . name } label = { label } fieldId = { `get-metrics-select-${ label }-${ id }` } helperText = { attribute . description } helperTextInvalid = { invalidText } validated = { invalid ? 'error' : 'default' } isRequired = { attribute . required } > < Select id = { id } value = { printableValue } onChange = { e => { return this . updateAttribute ( e , attribute ) ; } } > { ( ( ) => { if ( attribute . noDefaultDropdownOptionPlaceholder ) { return ( < SelectItem key = "default" label = { attribute . noDefaultDropdownOptionPlaceholder } value = { attribute . noDefaultDropdownOptionPlaceholder } / > ) ; } } ) ( ) } { attribute . dropdownOptions . map ( option => { return ( < SelectItem key = { option . printableOption } label = { option . printableOption } value = { option . printableOption } / > ) ; } ) } < /Select> < /FormGroup>); } } ; private renderMetricForm = ( selectedType , submitCallback , formType ) => { return ( < div > < Form onSubmit = { submitCallback } > { ( ( ) => { if ( selectedType === MetricTypes . counter ) { return COUNTER_METRIC_ATTRIBUTES_DATA . map ( attribute => this . renderAttributeForm ( attribute , formType ) ) ; } else { return RATIO_METRIC_ATTRIBUTES_DATA . map ( attribute => this . renderAttributeForm ( attribute , formType ) ) ; } } ) ( ) } < ActionGroup > < Button kind = "primary" type = "submit" > { ( ( ) => { if ( formType === FormTypes . add ) { if ( selectedType === MetricTypes . counter ) { return 'Create counter metric' ; } else { return 'Create ratio metric' ; } } else { if ( selectedType === MetricTypes . counter ) { return 'Edit counter metric' ; } else { return 'Edit ratio metric' ; } } } ) ( ) } < /Button> < Button className = "small-left-pad" kind = "link" type = "button" onClick = { this . displayGetMetrics } > Cancel < /Button> < /ActionGroup> < /Form> < /div>); } ; public render = ( ) => { const { display , selectedType , counterMetricsState , ratioMetricsState , errorString } = this . state ; switch ( display ) { case DisplayMode . getMetrics : return ( < div className = "pageStyle" > { this . renderMetricTable ( counterMetricsState , MetricTypes . counter , 'Counter Metrics' ) } { this . renderMetricTable ( ratioMetricsState , MetricTypes . ratio , 'Ratio Metrics' ) } < div className = "padding-content" > < Button kind = "primary" onClick = { this . refreshState } > Refresh < /Button> < /div> < /div>); case DisplayMode . addMetric : return ( < div className = "padding-content" > { ( ( ) => { return this . renderMetricForm ( selectedType , this . addMetricHandler , FormTypes . add ) ; } ) ( ) } < /div>); case DisplayMode . editMetric : return ( < div className = "padding-content" > { ( ( ) => { return this . renderMetricForm ( selectedType , this . editMetricHandler , FormTypes . edit ) ; } ) ( ) } < /div>); case DisplayMode . error : return < ErrorDisplay errorString = { errorString } > < /ErrorDisplay>; default : return < div > Cannot determine proper display mode < /div>; } } ; } type MetricsState = { [ metricName ] : MetricState ; } ; type MetricState = { name : string ; isDeleted : boolean ; alsoDelete : string [ ] ; alsoRestore : string [ ] ; details : CounterMetric | RatioMetric ; custom : boolean ; } ;