diff --git a/web/src/components/PluginsPanel/PluginsPanel.module.scss b/web/src/components/PluginsPanel/PluginsPanel.module.scss index b52a9eeaf..a29a0c08d 100644 --- a/web/src/components/PluginsPanel/PluginsPanel.module.scss +++ b/web/src/components/PluginsPanel/PluginsPanel.module.scss @@ -16,7 +16,7 @@ } .form { - height: 100%; + height: calc(100% - var(--spacing-large) - var(--spacing-xxlarge)); width: 100%; :global { .FormikForm--main { @@ -35,7 +35,7 @@ } .plugins { - max-height: calc(100vh - var(--header-height)); + max-height: calc(100vh - var(--header-height) - var(--generate-pipeline-header)); overflow-y: scroll; } @@ -49,6 +49,18 @@ overflow-y: scroll; } -.panelContent { - height: calc(100% - var(--spacing-large)); +.search { + width: 50%; + height: 35px; +} + +.indent { + background: var(--grey-100) !important; + padding: var(--spacing-medium) var(--spacing-medium) var(--spacing-small) var(--spacing-large) !important; + border-radius: 8px; +} + +.configForm { + height: calc(100% - var(--spacing-large) - var(--spacing-xxlarge)); + margin: var(--spacing-large) var(--spacing-xxlarge) var(--spacing-xxlarge) var(--spacing-xxlarge) !important; } diff --git a/web/src/components/PluginsPanel/PluginsPanel.module.scss.d.ts b/web/src/components/PluginsPanel/PluginsPanel.module.scss.d.ts index 544c2ca69..d839420bb 100644 --- a/web/src/components/PluginsPanel/PluginsPanel.module.scss.d.ts +++ b/web/src/components/PluginsPanel/PluginsPanel.module.scss.d.ts @@ -1,11 +1,13 @@ /* eslint-disable */ // This is an auto-generated file export declare const arrow: string +export declare const configForm: string export declare const form: string export declare const formFields: string -export declare const panelContent: string +export declare const indent: string export declare const plugin: string export declare const pluginDesc: string export declare const pluginDetailsPanel: string export declare const pluginIcon: string export declare const plugins: string +export declare const search: string diff --git a/web/src/components/PluginsPanel/PluginsPanel.tsx b/web/src/components/PluginsPanel/PluginsPanel.tsx index 2f598e204..a06e1aabe 100644 --- a/web/src/components/PluginsPanel/PluginsPanel.tsx +++ b/web/src/components/PluginsPanel/PluginsPanel.tsx @@ -1,14 +1,25 @@ 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 { Button, ButtonVariation, Container, FormInput, FormikForm, Layout, Popover, Text } from '@harnessio/uicore' +import { + Accordion, + Button, + ButtonVariation, + Container, + ExpandingSearchInput, + FormInput, + FormikForm, + Layout, + Popover, + Text +} from '@harnessio/uicore' import { useStrings } from 'framework/strings' -import pluginList from './plugins/plugins.json' - import css from './PluginsPanel.module.scss' enum PluginCategory { @@ -29,15 +40,6 @@ interface PluginInput { options?: { isExtended?: boolean } } -interface Plugin { - name: string - spec: { - name: string - description?: string - inputs: { [key: string]: PluginInput } - } -} - interface PluginCategoryInterface { category: PluginCategory name: string @@ -45,38 +47,6 @@ interface PluginCategoryInterface { icon: IconName } -const PluginCategories: PluginCategoryInterface[] = [ - { - category: PluginCategory.Harness, - name: 'Run', - description: 'Run a script on macOS, Linux, or Windows', - icon: 'run-step' - }, - { category: PluginCategory.Drone, name: 'Drone', description: 'Run Drone plugins', icon: 'ci-infra' } -] - -const StepNameInput: PluginInput = { - type: 'string', - description: 'Name of the step' -} - -const RunStep: Plugin = { - name: 'run', - spec: { - name: 'Run', - inputs: { - name: StepNameInput, - image: { - type: 'string' - }, - script: { - type: 'string', - options: { isExtended: true } - } - } - } -} - interface PluginInsertionTemplateInterface { name?: string type: 'plugin' @@ -101,6 +71,12 @@ const PluginInsertionTemplate: PluginInsertionTemplateInterface = { const PluginNameFieldPath = 'spec.name' const PluginInputsFieldPath = 'spec.inputs' +const LIST_FETCHING_LIMIT = 100 + +const RunStep: TypesPlugin = { + uid: 'run' +} + export interface PluginsPanelInterface { onPluginAddUpdate: (isUpdate: boolean, pluginFormData: Record) => void } @@ -109,23 +85,61 @@ export const PluginsPanel = ({ onPluginAddUpdate }: PluginsPanelInterface): JSX. const { getString } = useStrings() const [category, setCategory] = useState() const [panelView, setPanelView] = useState(PluginPanelView.Category) - const [plugin, setPlugin] = useState() - const [plugins, setPlugins] = useState() - const [loading] = useState(false) + const [plugin, setPlugin] = useState() + const [plugins, setPlugins] = useState([]) + const [query, setQuery] = useState('') + const [loading, setLoading] = useState(false) - const fetchPlugins = () => { - /* temporarily done till api response gets available */ - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - setPlugins(pluginList) + const PluginCategories: PluginCategoryInterface[] = [ + { + category: PluginCategory.Harness, + name: capitalize(getString('run')), + description: getString('pluginsPanel.run.helptext'), + icon: 'run-step' + }, + { + category: PluginCategory.Drone, + name: capitalize(getString('plugins.title')), + description: getString('pluginsPanel.plugins.helptext'), + icon: 'ci-infra' + } + ] + + const fetchPlugins = async (page: number): Promise => { + const response = await fetch(`/api/v1/plugins?page=${page}&limit=${LIST_FETCHING_LIMIT}`) + if (!response.ok) throw new Error('Failed to fetch plugins') + return response.json() } + const fetchAllPlugins = useCallback(async (): Promise => { + try { + setLoading(true) + const pluginsPage1 = await fetchPlugins(1) + const pluginsPage2 = await fetchPlugins(2) + setPlugins([...pluginsPage1, ...pluginsPage2]) + } catch (ex) { + /* ignore exception */ + } finally { + setLoading(false) + } + }, []) + useEffect(() => { if (category === PluginCategory.Drone) { - fetchPlugins() + fetchAllPlugins() } }, [category]) + useEffect(() => { + if (panelView !== PluginPanelView.Listing) return + + if (query) { + setPlugins(existingPlugins => existingPlugins.filter((item: TypesPlugin) => item.uid?.includes(query))) + } else { + fetchAllPlugins() + } + }, [query]) + const renderPluginCategories = (): JSX.Element => { return ( <> @@ -169,22 +183,32 @@ export const PluginsPanel = ({ onPluginAddUpdate }: PluginsPanelInterface): JSX. ) : ( - - { - setPanelView(PluginPanelView.Category) - }} - className={css.arrow} + + + { + setPanelView(PluginPanelView.Category) + }} + className={css.arrow} + /> + {getString('plugins.select')} + + - {plugins?.map((pluginItem: Plugin) => { - const { name: uid, description } = pluginItem.spec + {plugins?.map((pluginItem: TypesPlugin) => { + const { uid, description } = pluginItem return ( ) - }, [loading, plugins]) + }, [loading, plugins, query]) const generateFriendlyName = useCallback((pluginName: string): string => { return capitalize(pluginName.split('_').join(' ')) @@ -245,7 +269,7 @@ export const PluginsPanel = ({ onPluginAddUpdate }: PluginsPanelInterface): JSX. ) const renderPluginFormField = ({ name, properties }: { name: string; properties: PluginInput }): JSX.Element => { - const { type, default: defaultValue, options } = properties + const { type, options } = properties const { isExtended } = options || {} const WrapperComponent = isExtended ? FormInput.TextArea : FormInput.Text return type === 'string' ? ( @@ -254,7 +278,6 @@ export const PluginsPanel = ({ onPluginAddUpdate }: PluginsPanelInterface): JSX. label={generateLabelForPluginField({ name, properties })} style={{ width: '100%' }} key={name} - placeholder={defaultValue} /> ) : ( <> @@ -263,24 +286,29 @@ export const PluginsPanel = ({ onPluginAddUpdate }: PluginsPanelInterface): JSX. const constructPayloadForYAMLInsertion = ( pluginFormData: Record, - pluginMetadata?: Plugin + pluginMetadata?: TypesPlugin ): Record => { - const { name, image, script } = pluginFormData + const { name, container = {} } = pluginFormData switch (category) { case PluginCategory.Drone: - const payload = { ...PluginInsertionTemplate } - set(payload, 'name', name) - set(payload, PluginNameFieldPath, pluginMetadata?.name) + 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 image || script - ? { - ...(name && { name }), - type: 'run', - spec: { ...(image && { image }), ...(script && { script }) } - } - : {} + return { + ...(name && { name }), + type: 'run', + ...(Object.keys(container).length === 1 && container?.image + ? { spec: { container: get(container, 'image') } } + : { spec: pluginFormData }) + } default: return {} } @@ -291,20 +319,41 @@ export const PluginsPanel = ({ onPluginAddUpdate }: PluginsPanelInterface): JSX. }): { [key: string]: PluginInput } => { const inputsClone = Object.assign( { - name: StepNameInput + 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 getInitialFormValues = useCallback((pluginInputs: Record): Record => { + return Object.entries(pluginInputs).reduce((acc, [field, inputObj]) => { + if (inputObj?.default) { + acc[field] = inputObj.default + } + return acc + }, {} as Record) + }, []) + const renderPluginConfigForm = useCallback((): JSX.Element => { - const inputs: { [key: string]: PluginInput } = insertNameFieldToPluginInputs(get(plugin, 'spec.inputs', {})) + const pluginInputs = getPluginInputsFromSpec(get(plugin, 'spec', '') as string) + const allPluginInputs = insertNameFieldToPluginInputs(pluginInputs) return ( - + - {plugin?.spec?.name && ( + {plugin?.uid && ( - {getString('addLabel')} {plugin.spec.name} {getString('plugins.stepLabel')} + {getString('addLabel')} {plugin.uid} {getString('plugins.stepLabel')} )} { onPluginAddUpdate?.(false, constructPayloadForYAMLInsertion(formData, plugin)) }}> - - {inputs && ( - - {Object.keys(inputs).map((field: string) => { - return renderPluginFormField({ name: field, properties: get(inputs, field) }) - })} - - )} -