mirror of
https://github.com/harness/drone.git
synced 2025-05-17 01:20:13 +08:00
Merge branch 'add-list' of _OKE5H2PQKOUfzFFDuD4FA/default/CODE/gitness (#626)
This commit is contained in:
commit
032a7a89c8
23
web/src/components/MultiList/MultiList.module.scss
Normal file
23
web/src/components/MultiList/MultiList.module.scss
Normal 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;
|
||||||
|
}
|
20
web/src/components/MultiList/MultiList.module.scss.d.ts
vendored
Normal file
20
web/src/components/MultiList/MultiList.module.scss.d.ts
vendored
Normal 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
|
167
web/src/components/MultiList/MultiList.tsx
Normal file
167
web/src/components/MultiList/MultiList.tsx
Normal 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)
|
@ -68,6 +68,10 @@
|
|||||||
.formFields {
|
.formFields {
|
||||||
height: calc(100% - var(--spacing-xlarge));
|
height: calc(100% - var(--spacing-xlarge));
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
margin-left: var(--spacing-1) !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.search {
|
.search {
|
||||||
|
@ -29,3 +29,4 @@ export declare const pluginDetailsPanel: string
|
|||||||
export declare const pluginIcon: string
|
export declare const pluginIcon: string
|
||||||
export declare const plugins: string
|
export declare const plugins: string
|
||||||
export declare const search: string
|
export declare const search: string
|
||||||
|
export declare const toggle: string
|
||||||
|
@ -14,10 +14,10 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { Formik } from 'formik'
|
import { Formik, FormikContextType } from 'formik'
|
||||||
import { parse } from 'yaml'
|
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 { Classes, PopoverInteractionKind, PopoverPosition } from '@blueprintjs/core'
|
||||||
import { Color, FontVariation } from '@harnessio/design-system'
|
import { Color, FontVariation } from '@harnessio/design-system'
|
||||||
import { Icon, IconProps } from '@harnessio/icons'
|
import { Icon, IconProps } from '@harnessio/icons'
|
||||||
@ -36,6 +36,7 @@ import {
|
|||||||
} from '@harnessio/uicore'
|
} from '@harnessio/uicore'
|
||||||
import type { TypesPlugin } from 'services/code'
|
import type { TypesPlugin } from 'services/code'
|
||||||
import { useStrings } from 'framework/strings'
|
import { useStrings } from 'framework/strings'
|
||||||
|
import { MultiList } from 'components/MultiList/MultiList'
|
||||||
|
|
||||||
import css from './PluginsPanel.module.scss'
|
import css from './PluginsPanel.module.scss'
|
||||||
|
|
||||||
@ -47,8 +48,16 @@ export interface PluginForm {
|
|||||||
[key: string]: string | boolean | object
|
[key: string]: string | boolean | object
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ValueType {
|
||||||
|
STRING = 'string',
|
||||||
|
BOOLEAN = 'boolean',
|
||||||
|
NUMBER = 'number',
|
||||||
|
ARRAY = 'array',
|
||||||
|
OBJECT = 'object'
|
||||||
|
}
|
||||||
|
|
||||||
interface PluginInput {
|
interface PluginInput {
|
||||||
type: 'string'
|
type: ValueType
|
||||||
description?: string
|
description?: string
|
||||||
default?: string
|
default?: string
|
||||||
options?: { isExtended?: boolean }
|
options?: { isExtended?: boolean }
|
||||||
@ -114,6 +123,7 @@ export const PluginsPanel = ({ onPluginAddUpdate }: PluginsPanelInterface): JSX.
|
|||||||
const [plugins, setPlugins] = useState<TypesPlugin[]>([])
|
const [plugins, setPlugins] = useState<TypesPlugin[]>([])
|
||||||
const [query, setQuery] = useState<string>('')
|
const [query, setQuery] = useState<string>('')
|
||||||
const [loading, setLoading] = useState<boolean>(false)
|
const [loading, setLoading] = useState<boolean>(false)
|
||||||
|
const formikRef = useRef<FormikContextType<PluginForm>>()
|
||||||
|
|
||||||
const PluginCategories: PluginCategoryInterface[] = [
|
const PluginCategories: PluginCategoryInterface[] = [
|
||||||
{
|
{
|
||||||
@ -315,19 +325,51 @@ export const PluginsPanel = ({ onPluginAddUpdate }: PluginsPanelInterface): JSX.
|
|||||||
|
|
||||||
const renderPluginFormField = ({ name, properties }: { name: string; properties: PluginInput }): JSX.Element => {
|
const renderPluginFormField = ({ name, properties }: { name: string; properties: PluginInput }): JSX.Element => {
|
||||||
const { type, options } = properties
|
const { type, options } = properties
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case ValueType.STRING: {
|
||||||
const { isExtended } = options || {}
|
const { isExtended } = options || {}
|
||||||
const WrapperComponent = isExtended ? FormInput.TextArea : FormInput.Text
|
const WrapperComponent = isExtended ? FormInput.TextArea : FormInput.Text
|
||||||
return type === 'string' ? (
|
return (
|
||||||
<WrapperComponent
|
<WrapperComponent
|
||||||
name={name}
|
name={name}
|
||||||
label={generateLabelForPluginField({ name, properties })}
|
label={generateLabelForPluginField({ name, properties })}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
key={name}
|
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 constructPayloadForYAMLInsertion = (pluginFormData: PluginForm, pluginMetadata?: TypesPlugin): PluginForm => {
|
||||||
const { name, container = {} } = pluginFormData
|
const { name, container = {} } = pluginFormData
|
||||||
@ -422,8 +464,15 @@ export const PluginsPanel = ({ onPluginAddUpdate }: PluginsPanelInterface): JSX.
|
|||||||
<Formik<PluginForm>
|
<Formik<PluginForm>
|
||||||
initialValues={getInitialFormValues(pluginInputs)}
|
initialValues={getInitialFormValues(pluginInputs)}
|
||||||
onSubmit={(formData: PluginForm) => {
|
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' }}>
|
<FormikForm height="100%" flex={{ justifyContent: 'space-between', alignItems: 'baseline' }}>
|
||||||
<Layout.Vertical flex={{ alignItems: 'flex-start' }} height="inherit" spacing="medium">
|
<Layout.Vertical flex={{ alignItems: 'flex-start' }} height="inherit" spacing="medium">
|
||||||
<Layout.Vertical
|
<Layout.Vertical
|
||||||
@ -433,7 +482,12 @@ export const PluginsPanel = ({ onPluginAddUpdate }: PluginsPanelInterface): JSX.
|
|||||||
flex={{ justifyContent: 'space-between' }}>
|
flex={{ justifyContent: 'space-between' }}>
|
||||||
{category === PluginCategory.Harness ? (
|
{category === PluginCategory.Harness ? (
|
||||||
<Layout.Vertical width="inherit">
|
<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
|
<FormInput.TextArea
|
||||||
name={'script'}
|
name={'script'}
|
||||||
label={getString('pluginsPanel.run.script')}
|
label={getString('pluginsPanel.run.script')}
|
||||||
@ -567,6 +621,8 @@ export const PluginsPanel = ({ onPluginAddUpdate }: PluginsPanelInterface): JSX.
|
|||||||
</Container>
|
</Container>
|
||||||
</Layout.Vertical>
|
</Layout.Vertical>
|
||||||
</FormikForm>
|
</FormikForm>
|
||||||
|
)
|
||||||
|
}}
|
||||||
</Formik>
|
</Formik>
|
||||||
</Container>
|
</Container>
|
||||||
</Layout.Vertical>
|
</Layout.Vertical>
|
||||||
|
Loading…
Reference in New Issue
Block a user