refactor as per new plugin interfaces

This commit is contained in:
Vardan Bansal 2023-09-13 22:50:06 -07:00
parent 0799c2b0f0
commit c8ba6321c2
4 changed files with 151 additions and 92 deletions

View File

@ -43,3 +43,12 @@
text-overflow: ellipsis;
width: calc(30vw - 2 * var(--spacing-huge));
}
.formFields {
height: calc(100% - var(--spacing-xlarge));
overflow-y: scroll;
}
.panelContent {
height: calc(100% - var(--spacing-large));
}

View File

@ -2,6 +2,8 @@
// This is an auto-generated file
export declare const arrow: string
export declare const form: string
export declare const formFields: string
export declare const panelContent: string
export declare const plugin: string
export declare const pluginDesc: string
export declare const pluginDetailsPanel: string

View File

@ -1,12 +1,11 @@
import React, { useCallback, useEffect, useState } from 'react'
import { Formik } from 'formik'
import { capitalize, get } from 'lodash-es'
import { Classes, PopoverInteractionKind, PopoverPosition } from '@blueprintjs/core'
import { Color, FontVariation } from '@harnessio/design-system'
import { Icon, type IconName } from '@harnessio/icons'
import { Button, ButtonVariation, Container, FormInput, FormikForm, Layout, Text } from '@harnessio/uicore'
import { Button, ButtonVariation, Container, FormInput, FormikForm, Layout, Popover, Text } from '@harnessio/uicore'
import { useStrings } from 'framework/strings'
import type { TypesPlugin } from 'services/code'
import { YamlVersion } from 'pages/AddUpdatePipeline/Constants'
import pluginList from './plugins/plugins.json'
@ -23,14 +22,30 @@ enum PluginPanelView {
Configuration
}
interface PluginInterface {
interface PluginInput {
type: 'string'
description?: string
default?: string
options?: { isExtended?: boolean }
}
interface Plugin {
name: string
spec: {
name: string
description?: string
inputs: { [key: string]: PluginInput }
}
}
interface PluginCategoryInterface {
category: PluginCategory
name: string
description: string
icon: IconName
}
const PluginCategories: PluginInterface[] = [
const PluginCategories: PluginCategoryInterface[] = [
{
category: PluginCategory.Harness,
name: 'Run',
@ -40,50 +55,34 @@ const PluginCategories: PluginInterface[] = [
{ category: PluginCategory.Drone, name: 'Drone', description: 'Run Drone plugins', icon: 'ci-infra' }
]
const dronePluginSpecMockData = {
inputs: {
channel: {
type: 'string'
},
token: {
type: 'string'
}
},
steps: [
{
type: 'script',
spec: {
image: 'plugins/slack'
},
envs: {
PLUGIN_CHANNEL: '<+inputs.channel>'
const RunStep: Plugin = {
name: 'run',
spec: {
name: 'Run',
inputs: {
script: {
type: 'string',
options: { isExtended: true }
}
}
]
}
const runStepSpec = {
inputs: {
script: {
type: 'string'
}
}
}
export interface PluginsPanelInterface {
version?: YamlVersion
onPluginAddUpdate: (isUpdate: boolean, pluginFormData: Record<string, any>) => void
}
export const PluginsPanel = ({ version = YamlVersion.V0, onPluginAddUpdate }: PluginsPanelInterface): JSX.Element => {
export const PluginsPanel = ({ onPluginAddUpdate }: PluginsPanelInterface): JSX.Element => {
const { getString } = useStrings()
const [category, setCategory] = useState<PluginCategory>()
const [panelView, setPanelView] = useState<PluginPanelView>(PluginPanelView.Category)
const [plugin, setPlugin] = useState<TypesPlugin>()
const [plugins, setPlugins] = useState<Record<string, any>[]>()
const [plugin, setPlugin] = useState<Plugin>()
const [plugins, setPlugins] = useState<Plugin[]>()
const [loading] = useState<boolean>(false)
const fetchPlugins = () => {
/* temporarily done till api response gets available */
//@ts-ignore
setPlugins(pluginList)
}
@ -96,7 +95,7 @@ export const PluginsPanel = ({ version = YamlVersion.V0, onPluginAddUpdate }: Pl
const renderPluginCategories = (): JSX.Element => {
return (
<>
{PluginCategories.map((item: PluginInterface) => {
{PluginCategories.map((item: PluginCategoryInterface) => {
const { name, category: pluginCategory, description, icon } = item
return (
<Layout.Horizontal
@ -105,7 +104,7 @@ export const PluginsPanel = ({ version = YamlVersion.V0, onPluginAddUpdate }: Pl
if (pluginCategory === PluginCategory.Drone) {
setPanelView(PluginPanelView.Listing)
} else if (pluginCategory === PluginCategory.Harness) {
setPlugin({ uid: getString('run') })
setPlugin(RunStep)
setPanelView(PluginPanelView.Configuration)
}
}}
@ -150,8 +149,8 @@ export const PluginsPanel = ({ version = YamlVersion.V0, onPluginAddUpdate }: Pl
/>
</Layout.Horizontal>
<Container className={css.plugins}>
{plugins?.map((_plugin: Record<string, any>) => {
const { name: uid, description } = _plugin.spec
{plugins?.map((pluginItem: Plugin) => {
const { name: uid, description } = pluginItem.spec
return (
<Layout.Horizontal
flex={{ justifyContent: 'flex-start' }}
@ -159,7 +158,7 @@ export const PluginsPanel = ({ version = YamlVersion.V0, onPluginAddUpdate }: Pl
className={css.plugin}
onClick={() => {
setPanelView(PluginPanelView.Configuration)
setPlugin(_plugin)
setPlugin(pluginItem)
}}
key={uid}>
<Icon name={'gear'} size={25} />
@ -179,40 +178,74 @@ export const PluginsPanel = ({ version = YamlVersion.V0, onPluginAddUpdate }: Pl
)
}, [loading, plugins])
const renderPluginFormField = ({ name, type }: { name: string; type: 'string' }): JSX.Element => {
const generateFriendlyName = useCallback((pluginName: string): string => {
return capitalize(pluginName.split('_').join(' '))
}, [])
const generateLabelForPluginField = useCallback(
({ name, properties }: { name: string; properties: PluginInput }): JSX.Element | string => {
const { description } = properties
return (
<Layout.Horizontal spacing="small" flex={{ alignItems: 'center', justifyContent: 'flex-start' }}>
{name && <Text font={{ variation: FontVariation.FORM_LABEL }}>{generateFriendlyName(name)}</Text>}
{description && (
<Popover
interactionKind={PopoverInteractionKind.HOVER}
boundary="viewport"
position={PopoverPosition.RIGHT}
popoverClassName={Classes.DARK}
content={
<Container padding="medium">
<Text font={{ variation: FontVariation.SMALL }} color={Color.WHITE}>
{description}
</Text>
</Container>
}>
<Icon name="info" color={Color.PRIMARY_7} size={10} padding={{ bottom: 'small' }} />
</Popover>
)}
</Layout.Horizontal>
)
},
[]
)
const renderPluginFormField = ({ name, properties }: { name: string; properties: PluginInput }): JSX.Element => {
const { type, default: defaultValue, options } = properties
const { isExtended } = options || {}
const WrapperComponent = isExtended ? FormInput.TextArea : FormInput.Text
return type === 'string' ? (
<FormInput.Text
<WrapperComponent
name={name}
label={<Text font={{ variation: FontVariation.FORM_INPUT_TEXT }}>{capitalize(name)}</Text>}
label={generateLabelForPluginField({ name, properties })}
style={{ width: '100%' }}
key={name}
placeholder={defaultValue}
/>
) : (
<></>
)
}
const constructPayloadForYAMLInsertion = (isUpdate: boolean, pluginFormData: Record<string, any>) => {
const constructPayloadForYAMLInsertion = (pluginFormData: Record<string, any>): Record<string, any> => {
let constructedPayload = { ...pluginFormData }
switch (category) {
case PluginCategory.Drone:
return { type: 'script', spec: constructedPayload }
case PluginCategory.Harness:
constructedPayload =
version === YamlVersion.V1
? { type: 'script', spec: { run: get(constructedPayload, 'script', '') } }
: { name: 'run step', commands: [get(constructedPayload, 'script', '')] }
return { type: 'script', spec: { run: get(constructedPayload, 'script', '') } }
default:
return {}
}
onPluginAddUpdate?.(isUpdate, constructedPayload)
}
const renderPluginConfigForm = useCallback((): JSX.Element => {
// TODO obtain plugin input spec by parsing YAML
const inputs = get(category === PluginCategory.Drone ? dronePluginSpecMockData : runStepSpec, 'inputs', {})
const inputs: { [key: string]: PluginInput } = get(plugin, 'spec.inputs', {})
return (
<Layout.Vertical
spacing="medium"
margin={{ left: 'xxlarge', top: 'large', right: 'xxlarge', bottom: 'xxlarge' }}
height="95%">
spacing="large"
padding={{ left: 'xxlarge', top: 'large', right: 'xxlarge', bottom: 'xxlarge' }}
className={css.panelContent}>
<Layout.Horizontal spacing="small" flex={{ justifyContent: 'flex-start' }}>
<Icon
name="arrow-left"
@ -227,28 +260,27 @@ export const PluginsPanel = ({ version = YamlVersion.V0, onPluginAddUpdate }: Pl
}}
className={css.arrow}
/>
{plugin?.uid ? (
{plugin?.spec?.name && (
<Text font={{ variation: FontVariation.H5 }}>
{getString('addLabel')} {plugin.uid} {getString('plugins.stepLabel')}
{getString('addLabel')} {plugin.spec.name} {getString('plugins.stepLabel')}
</Text>
) : (
<></>
)}
</Layout.Horizontal>
<Container className={css.form}>
<Formik
initialValues={{}}
onSubmit={formData => {
constructPayloadForYAMLInsertion(false, formData)
onPluginAddUpdate?.(false, constructPayloadForYAMLInsertion(formData))
}}>
<FormikForm>
<FormikForm height="100%" flex={{ justifyContent: 'space-between', alignItems: 'baseline' }}>
<Layout.Vertical flex={{ alignItems: 'flex-start' }} height="100%">
<Layout.Vertical width="100%">
{Object.keys(inputs).map((field: string) => {
const fieldType = get(inputs, `${field}.type`, '') as 'string'
return renderPluginFormField({ name: field, type: fieldType })
})}
</Layout.Vertical>
{inputs && (
<Layout.Vertical width="100%" className={css.formFields} spacing="xsmall">
{Object.keys(inputs).map((field: string) => {
return renderPluginFormField({ name: field, properties: get(inputs, field) })
})}
</Layout.Vertical>
)}
<Button variation={ButtonVariation.PRIMARY} text={getString('addLabel')} type="submit" />
</Layout.Vertical>
</FormikForm>
@ -272,7 +304,7 @@ export const PluginsPanel = ({ version = YamlVersion.V0, onPluginAddUpdate }: Pl
}, [loading, plugins, panelView, category])
return (
<Layout.Vertical>
<Layout.Vertical height="100%">
{panelView === PluginPanelView.Category ? (
<Container padding={{ top: 'medium', bottom: 'medium', left: 'medium' }}>
<Text font={{ variation: FontVariation.H5 }}>{getString('step.select')}</Text>

View File

@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useGet, useMutate } from 'restful-react'
import { Link, useParams } from 'react-router-dom'
import { get, isEmpty, isUndefined, set } from 'lodash-es'
import { stringify } from 'yaml'
import { parse, stringify } from 'yaml'
import { Menu, PopoverPosition } from '@blueprintjs/core'
import {
Container,
@ -91,7 +91,6 @@ const AddUpdatePipeline = (): JSX.Element => {
const { repoMetadata } = useGetRepositoryMetadata()
const { showError, showSuccess, clear: clearToaster } = useToaster()
const [yamlVersion, setYAMLVersion] = useState<YamlVersion>()
const [pipelineAsObj, setPipelineAsObj] = useState<Record<string, any>>({})
const [pipelineAsYAML, setPipelineAsYaml] = useState<string>('')
const { openModal: openRunPipelineModal } = useRunPipelineModal()
const repoPath = useMemo(() => repoMetadata?.path || '', [repoMetadata])
@ -169,7 +168,7 @@ const AddUpdatePipeline = (): JSX.Element => {
// ignore exception
}
}
}, [yamlVersion, isExistingPipeline, originalPipelineYAMLFileContent, pipelineAsObj])
}, [yamlVersion, isExistingPipeline, originalPipelineYAMLFileContent])
// find if editor content was modified
useEffect(() => {
@ -217,28 +216,40 @@ const AddUpdatePipeline = (): JSX.Element => {
}
}
const updatePipeline = (payload: Record<string, any>): Record<string, any> => {
const pipelineAsObjClone = { ...pipelineAsObj }
const stepInsertPath = yamlVersion === YamlVersion.V1 ? 'spec.stages.0.spec.steps' : 'steps'
let existingSteps: [unknown] = get(pipelineAsObjClone, stepInsertPath, [])
if (existingSteps.length > 0) {
existingSteps.push(payload)
} else {
existingSteps = [payload]
const updatePipelineWithPluginData = (
existingPipeline: Record<string, any>,
payload: Record<string, any>
): Record<string, any> => {
const pipelineAsObjClone = { ...existingPipeline }
if (Object.keys(pipelineAsObjClone).length > 0) {
const stepInsertPath = 'spec.stages.0.spec.steps'
let existingSteps: [unknown] = get(pipelineAsObjClone, stepInsertPath, [])
if (existingSteps.length > 0) {
existingSteps.push(payload)
} else {
existingSteps = [payload]
}
set(pipelineAsObjClone, stepInsertPath, existingSteps)
return pipelineAsObjClone
}
set(pipelineAsObjClone, stepInsertPath, existingSteps)
return pipelineAsObjClone
return existingPipeline
}
const addUpdatePluginToPipelineYAML = (_isUpdate: boolean, pluginFormData: Record<string, any>): void => {
try {
const updatedPipelineAsObj = updatePipeline(pluginFormData)
setPipelineAsObj(updatedPipelineAsObj)
setPipelineAsYaml(stringify(updatedPipelineAsObj))
} catch (ex) {
// ignore exception
}
}
const handlePluginAddUpdateIntoYAML = useCallback(
(_isUpdate: boolean, pluginFormData: Record<string, any>): void => {
try {
const pipeline = parse(pipelineAsYAML)
const updatedPipelineAsObj = updatePipelineWithPluginData(pipeline, pluginFormData)
if (Object.keys(updatedPipelineAsObj).length > 0) {
// avoid setting to empty pipeline in case pipeline update with plugin data fails
setPipelineAsYaml(stringify(updatedPipelineAsObj))
}
} catch (ex) {
// ignore exception
}
},
[yamlVersion, isExistingPipeline, originalPipelineYAMLFileContent, pipelineAsYAML]
)
const renderCTA = useCallback(() => {
switch (selectedOption?.action) {
@ -300,6 +311,10 @@ const AddUpdatePipeline = (): JSX.Element => {
}
}, [loading, fetchingPipeline, isDirty, repoMetadata, pipeline, selectedOption, isExistingPipeline, pipelineAsYAML])
if (fetchingPipeline || fetchingPipelineYAMLFileContent) {
return <LoadingSpinner visible={true} />
}
return (
<>
<Container className={css.main}>
@ -317,7 +332,6 @@ const AddUpdatePipeline = (): JSX.Element => {
content={<Layout.Horizontal flex={{ justifyContent: 'space-between' }}>{renderCTA()}</Layout.Horizontal>}
/>
<PageBody>
<LoadingSpinner visible={fetchingPipeline || fetchingPipelineYAMLFileContent} />
<Layout.Horizontal className={css.container}>
<Container className={css.editorContainer}>
<MonacoSourceCodeEditor
@ -327,9 +341,11 @@ const AddUpdatePipeline = (): JSX.Element => {
onChange={(value: string) => setPipelineAsYaml(value)}
/>
</Container>
<Container className={css.pluginsContainer}>
<PluginsPanel onPluginAddUpdate={addUpdatePluginToPipelineYAML} version={yamlVersion} />
</Container>
{yamlVersion === YamlVersion.V1 && (
<Container className={css.pluginsContainer}>
<PluginsPanel onPluginAddUpdate={handlePluginAddUpdateIntoYAML} />
</Container>
)}
</Layout.Horizontal>
</PageBody>
</Container>