import React, { useCallback, useEffect, useState } from 'react' import { Formik } from 'formik' import { parse } from 'yaml' import { capitalize, get, omit, set } from 'lodash-es' import { Classes, PopoverInteractionKind, PopoverPosition } from '@blueprintjs/core' import type { TypesPlugin } from 'services/code' import { Color, FontVariation } from '@harnessio/design-system' import { Icon, type IconName } from '@harnessio/icons' import { Accordion, Button, ButtonVariation, Container, ExpandingSearchInput, FormInput, FormikForm, Layout, Popover, Text } from '@harnessio/uicore' import { useStrings } from 'framework/strings' import css from './PluginsPanel.module.scss' enum PluginCategory { Harness, Drone } enum PluginPanelView { Category, Listing, Configuration } interface PluginInput { type: 'string' description?: string default?: string options?: { isExtended?: boolean } } interface PluginCategoryInterface { category: PluginCategory name: string description: string icon: IconName } const RunStep: TypesPlugin = { uid: 'run', description: 'Run a script', spec: '{"kind":"run","type":"step","name":"Run","spec":{"name":"run","description":"Run a script","inputs":{"image":{"type":"string","description":"Container image","required":true},"script":{"type":"string","description":"Script to execute","required":true,"options":{"isExtended":true}}}}}' } interface PluginInsertionTemplateInterface { name?: string type: 'plugin' spec: { name: string inputs: { [key: string]: string } } } const PluginInsertionTemplate: PluginInsertionTemplateInterface = { name: '', type: 'plugin', spec: { name: '', inputs: { '': '', '': '' } } } const PluginNameFieldPath = 'spec.name' const PluginInputsFieldPath = 'spec.inputs' const LIST_FETCHING_LIMIT = 100 export interface PluginsPanelInterface { onPluginAddUpdate: (isUpdate: boolean, pluginFormData: Record) => void } export const PluginsPanel = ({ onPluginAddUpdate }: PluginsPanelInterface): JSX.Element => { const { getString } = useStrings() const [category, setCategory] = useState() const [panelView, setPanelView] = useState(PluginPanelView.Category) const [plugin, setPlugin] = useState() const [plugins, setPlugins] = useState([]) const [query, setQuery] = useState('') const [loading, setLoading] = useState(false) const PluginCategories: PluginCategoryInterface[] = [ { category: PluginCategory.Harness, name: 'Run', description: getString('pluginsPanel.run.helptext'), icon: 'run-step' }, { category: PluginCategory.Drone, name: 'Plugin', description: getString('pluginsPanel.plugins.helptext'), icon: 'ci-infra' } ] const fetchAllPlugins = useCallback((): void => { try { setLoading(true) let allPlugins: TypesPlugin[] = [] fetch(`/api/v1/plugins?page=${1}&limit=${LIST_FETCHING_LIMIT}`) .then(async response => { const plugins = await response.json() allPlugins = [...plugins] fetch(`/api/v1/plugins?page=${2}&limit=${LIST_FETCHING_LIMIT}`).then(async response => { const plugins = await response.json() setPlugins([...allPlugins, ...plugins]) }) }) .catch(_err => { /* ignore error */ }) .catch(_err => { /* ignore error */ }) setLoading(false) } catch (ex) { /* ignore exception */ setLoading(false) } }, []) useEffect(() => { if (category === PluginCategory.Drone) { fetchAllPlugins() } }, [category]) useEffect(() => { if (panelView === PluginPanelView.Listing) { if (query) { setPlugins(existingPlugins => existingPlugins.filter((item: TypesPlugin) => item.uid?.includes(query))) } else { fetchAllPlugins() } } }, [query]) const renderPluginCategories = (): JSX.Element => { return ( <> {PluginCategories.map((item: PluginCategoryInterface) => { const { name, category: pluginCategory, description, icon } = item return ( { setCategory(pluginCategory) if (pluginCategory === PluginCategory.Drone) { setPanelView(PluginPanelView.Listing) } else if (pluginCategory === PluginCategory.Harness) { setPlugin(RunStep) setPanelView(PluginPanelView.Configuration) } }} key={pluginCategory} padding={{ left: 'medium', right: 'medium', top: 'medium', bottom: 'medium' }} flex={{ justifyContent: 'flex-start' }} className={css.plugin}> {name} {description} ) })} ) } const renderPlugins = useCallback((): JSX.Element => { return loading ? ( ) : ( { setPanelView(PluginPanelView.Category) }} className={css.arrow} /> {getString('plugins.select')} {plugins?.map((pluginItem: TypesPlugin) => { const { uid, description } = pluginItem return ( { setPanelView(PluginPanelView.Configuration) setPlugin(pluginItem) }} key={uid}> {uid} {description} ) })} ) }, [loading, plugins, query]) 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 ( {name && {generateFriendlyName(name)}} {description && ( {description} }> )} ) }, [] ) const renderPluginFormField = ({ name, properties }: { name: string; properties: PluginInput }): JSX.Element => { const { type, options } = properties const { isExtended } = options || {} const WrapperComponent = isExtended ? FormInput.TextArea : FormInput.Text return type === 'string' ? ( ) : ( <> ) } const constructPayloadForYAMLInsertion = ( pluginFormData: Record, pluginMetadata?: TypesPlugin ): Record => { const { name } = pluginFormData switch (category) { case PluginCategory.Drone: let payload = { ...PluginInsertionTemplate } /* Step name is optional, set only if specified by user */ if (name) { set(payload, 'name', name) } else { payload = omit(payload, 'name') } set(payload, PluginNameFieldPath, pluginMetadata?.uid) set(payload, PluginInputsFieldPath, omit(pluginFormData, 'name')) return payload as PluginInsertionTemplateInterface case PluginCategory.Harness: return { ...(name && { name }), type: 'run', spec: pluginFormData } default: return {} } } const insertNameFieldToPluginInputs = (existingInputs: { [key: string]: PluginInput }): { [key: string]: PluginInput } => { const inputsClone = Object.assign( { name: { type: 'string', description: 'Name of the step' } }, existingInputs ) return inputsClone } const getPluginInputsFromSpec = useCallback((pluginSpec: string): Record => { if (!pluginSpec) { return {} } try { const pluginSpecAsObj = parse(pluginSpec) return get(pluginSpecAsObj, 'spec.inputs', {}) } catch (ex) {} return {} }, []) const renderPluginConfigForm = useCallback((): JSX.Element => { const pluginInputs = getPluginInputsFromSpec(get(plugin, 'spec', '') as string) if (category === PluginCategory.Drone && Object.keys(pluginInputs).length === 0) { return <> } const allPluginInputs = insertNameFieldToPluginInputs(pluginInputs) return ( { setPlugin(undefined) if (category === PluginCategory.Drone) { setPanelView(PluginPanelView.Listing) } else if (category === PluginCategory.Harness) { setPanelView(PluginPanelView.Category) } }} className={css.arrow} /> {plugin?.uid && ( {getString('addLabel')} {plugin.uid} {getString('plugins.stepLabel')} )} { onPluginAddUpdate?.(false, constructPayloadForYAMLInsertion(formData, plugin)) }}> {category === PluginCategory.Harness ? ( } /> } /> } /> ) : ( {Object.keys(allPluginInputs).map((field: string) => { return renderPluginFormField({ name: field, properties: get(allPluginInputs, field) }) })} )}