Merge branch 'add-list' of _OKE5H2PQKOUfzFFDuD4FA/default/CODE/gitness (#626)

This commit is contained in:
vardan.bansal@harness.io vardan 2023-09-28 21:28:01 +00:00 committed by Harness
commit 032a7a89c8
6 changed files with 431 additions and 160 deletions

View File

@ -0,0 +1,23 @@
/*
* Copyright 2023 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.addBtn {
width: fit-content;
}
.deleteRowBtn {
cursor: pointer;
}

View File

@ -0,0 +1,20 @@
/*
* Copyright 2023 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable */
// This is an auto-generated file
export declare const addBtn: string
export declare const deleteRowBtn: string

View File

@ -0,0 +1,167 @@
/*
* Copyright 2023 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { debounce, has, omit } from 'lodash'
import { FormikContextType, connect } from 'formik'
import { Layout, Text, FormInput, Button, ButtonVariation, ButtonSize, Container } from '@harnessio/uicore'
import { FontVariation } from '@harnessio/design-system'
import { Icon } from '@harnessio/icons'
import { useStrings } from 'framework/strings'
import css from './MultiList.module.scss'
interface MultiListConnectedProps extends MultiListProps {
formik?: FormikContextType<any>
}
interface MultiListProps {
name: string
label: string
readOnly?: boolean
}
/* Allows user to create following structure:
<field-name>:
- <field-value-1>,
- <field-value-2>,
...
*/
export const MultiList = ({ name, label, readOnly, formik }: MultiListConnectedProps): JSX.Element => {
const { getString } = useStrings()
const [valueMap, setValueMap] = useState<Map<string, string>>(new Map<string, string>([]))
/*
<field-name-1>: <field-value-1>,
<field-name-2>: <field-value-2>,
...
*/
const counter = useRef<number>(0)
useEffect(() => {
const values = Array.from(valueMap.values())
if (values.length > 0) {
formik?.setFieldValue(name, values)
} else {
cleanupField()
}
}, [valueMap])
const cleanupField = useCallback((): void => {
formik?.setValues(omit({ ...formik?.values }, name))
}, [formik?.values])
const getFieldName = useCallback(
(index: number): string => {
return `${name}-${index}`
},
[name]
)
const handleAddRowToList = useCallback((): void => {
setValueMap((existingValueMap: Map<string, string>) => {
const rowKeyToAdd = getFieldName(counter.current)
if (!existingValueMap.has(rowKeyToAdd)) {
const existingValueMapClone = new Map(existingValueMap)
existingValueMapClone.set(rowKeyToAdd, '') /* Add key <field-name-1>, <field-name-2>, ... */
counter.current++ /* this counter always increases, even if a row is removed. This ensures no key collision in the existing value map. */
return existingValueMapClone
}
return existingValueMap
})
}, [])
const handleRemoveRowFromList = useCallback((removedRowKey: string): void => {
setValueMap((existingValueMap: Map<string, string>) => {
if (existingValueMap.has(removedRowKey)) {
const existingValueMapClone = new Map(existingValueMap)
existingValueMapClone.delete(removedRowKey)
return existingValueMapClone
}
return existingValueMap
})
/* remove <field-name-1>, <field-name-2>, ... from formik values, if exist */
if (removedRowKey && has(formik?.values, removedRowKey)) {
formik?.setValues(omit({ ...formik?.values }, removedRowKey))
}
}, [])
const handleAddItemToRow = useCallback((rowKey: string, insertedValue: string): void => {
setValueMap((existingValueMap: Map<string, string>) => {
if (existingValueMap.has(rowKey)) {
const existingValueMapClone = new Map(existingValueMap)
existingValueMapClone.set(rowKey, insertedValue)
return existingValueMapClone
}
return existingValueMap
})
}, [])
const debouncedAddItemToList = useCallback(debounce(handleAddItemToRow, 500), [handleAddItemToRow])
const renderRow = useCallback((rowKey: string): React.ReactElement => {
return (
<Layout.Horizontal margin={{ bottom: 'none' }} flex={{ justifyContent: 'space-between', alignItems: 'center' }}>
<Container width="90%">
<FormInput.Text
name={rowKey}
disabled={readOnly}
onChange={event => {
const value = (event.target as HTMLInputElement).value
debouncedAddItemToList(rowKey, value)
}}
/>
</Container>
<Icon
name="code-delete"
size={25}
padding={{ bottom: 'medium' }}
className={css.deleteRowBtn}
onClick={event => {
event.preventDefault()
handleRemoveRowFromList(rowKey)
}}
/>
</Layout.Horizontal>
)
}, [])
const renderRows = useCallback((): React.ReactElement => {
const rows: React.ReactElement[] = []
valueMap.forEach((_value: string, key: string) => {
rows.push(renderRow(key))
})
return <Layout.Vertical>{rows}</Layout.Vertical>
}, [valueMap])
return (
<Layout.Vertical spacing="small">
<Layout.Vertical>
<Text font={{ variation: FontVariation.FORM_LABEL }}>{label}</Text>
{valueMap.size > 0 && <Container padding={{ top: 'small' }}>{renderRows()}</Container>}
</Layout.Vertical>
<Button
text={getString('addLabel')}
rightIcon="plus"
variation={ButtonVariation.PRIMARY}
size={ButtonSize.SMALL}
className={css.addBtn}
onClick={handleAddRowToList}
/>
</Layout.Vertical>
)
}
export default connect(MultiList)

View File

@ -68,6 +68,10 @@
.formFields {
height: calc(100% - var(--spacing-xlarge));
overflow-y: scroll;
.toggle {
margin-left: var(--spacing-1) !important;
}
}
.search {

View File

@ -29,3 +29,4 @@ export declare const pluginDetailsPanel: string
export declare const pluginIcon: string
export declare const plugins: string
export declare const search: string
export declare const toggle: string

View File

@ -14,10 +14,10 @@
* limitations under the License.
*/
import React, { useCallback, useEffect, useState } from 'react'
import { Formik } from 'formik'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { Formik, FormikContextType } from 'formik'
import { parse } from 'yaml'
import { capitalize, get, has, omit, set } from 'lodash-es'
import { capitalize, get, has, omit, pick, set } from 'lodash-es'
import { Classes, PopoverInteractionKind, PopoverPosition } from '@blueprintjs/core'
import { Color, FontVariation } from '@harnessio/design-system'
import { Icon, IconProps } from '@harnessio/icons'
@ -36,6 +36,7 @@ import {
} from '@harnessio/uicore'
import type { TypesPlugin } from 'services/code'
import { useStrings } from 'framework/strings'
import { MultiList } from 'components/MultiList/MultiList'
import css from './PluginsPanel.module.scss'
@ -47,8 +48,16 @@ export interface PluginForm {
[key: string]: string | boolean | object
}
enum ValueType {
STRING = 'string',
BOOLEAN = 'boolean',
NUMBER = 'number',
ARRAY = 'array',
OBJECT = 'object'
}
interface PluginInput {
type: 'string'
type: ValueType
description?: string
default?: string
options?: { isExtended?: boolean }
@ -114,6 +123,7 @@ export const PluginsPanel = ({ onPluginAddUpdate }: PluginsPanelInterface): JSX.
const [plugins, setPlugins] = useState<TypesPlugin[]>([])
const [query, setQuery] = useState<string>('')
const [loading, setLoading] = useState<boolean>(false)
const formikRef = useRef<FormikContextType<PluginForm>>()
const PluginCategories: PluginCategoryInterface[] = [
{
@ -315,19 +325,51 @@ export const PluginsPanel = ({ onPluginAddUpdate }: PluginsPanelInterface): JSX.
const renderPluginFormField = ({ name, properties }: { name: string; properties: PluginInput }): JSX.Element => {
const { type, options } = properties
switch (type) {
case ValueType.STRING: {
const { isExtended } = options || {}
const WrapperComponent = isExtended ? FormInput.TextArea : FormInput.Text
return type === 'string' ? (
return (
<WrapperComponent
name={name}
label={generateLabelForPluginField({ name, properties })}
style={{ width: '100%' }}
key={name}
/>
) : (
<></>
)
}
case ValueType.BOOLEAN:
return (
<Container className={css.toggle}>
<FormInput.Toggle
name={name}
label={generateLabelForPluginField({ name, properties }) as string}
style={{ width: '100%' }}
key={name}
/>
</Container>
)
case ValueType.ARRAY:
return (
<Container margin={{ bottom: 'large' }}>
<MultiList
name={name}
label={generateLabelForPluginField({ name, properties }) as string}
formik={formikRef.current}
/>
</Container>
)
default:
return <></>
}
}
/* Ensures no junk/unrecognized form values are set in the YAML */
const sanitizeFormData = useCallback((existingFormData: PluginForm, pluginInputs: PluginInputs): PluginForm => {
return pick(existingFormData, Object.keys(pluginInputs))
}, [])
const constructPayloadForYAMLInsertion = (pluginFormData: PluginForm, pluginMetadata?: TypesPlugin): PluginForm => {
const { name, container = {} } = pluginFormData
@ -422,8 +464,15 @@ export const PluginsPanel = ({ onPluginAddUpdate }: PluginsPanelInterface): JSX.
<Formik<PluginForm>
initialValues={getInitialFormValues(pluginInputs)}
onSubmit={(formData: PluginForm) => {
onPluginAddUpdate?.(false, constructPayloadForYAMLInsertion(formData, plugin))
}}>
onPluginAddUpdate?.(
false,
constructPayloadForYAMLInsertion(sanitizeFormData(formData, pluginInputs), plugin)
)
}}
validate={(formData: PluginForm) => console.log(formData)}>
{formik => {
formikRef.current = formik
return (
<FormikForm height="100%" flex={{ justifyContent: 'space-between', alignItems: 'baseline' }}>
<Layout.Vertical flex={{ alignItems: 'flex-start' }} height="inherit" spacing="medium">
<Layout.Vertical
@ -433,7 +482,12 @@ export const PluginsPanel = ({ onPluginAddUpdate }: PluginsPanelInterface): JSX.
flex={{ justifyContent: 'space-between' }}>
{category === PluginCategory.Harness ? (
<Layout.Vertical width="inherit">
<FormInput.Text name={'name'} label={getString('name')} style={{ width: '100%' }} key={'name'} />
<FormInput.Text
name={'name'}
label={getString('name')}
style={{ width: '100%' }}
key={'name'}
/>
<FormInput.TextArea
name={'script'}
label={getString('pluginsPanel.run.script')}
@ -567,6 +621,8 @@ export const PluginsPanel = ({ onPluginAddUpdate }: PluginsPanelInterface): JSX.
</Container>
</Layout.Vertical>
</FormikForm>
)
}}
</Formik>
</Container>
</Layout.Vertical>