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' import { stringify } from 'yaml' import { Menu, PopoverPosition } from '@blueprintjs/core' import { Container, PageHeader, PageBody, Layout, ButtonVariation, Text, useToaster, SplitButton, Button } from '@harnessio/uicore' import { Icon } from '@harnessio/icons' import { Color, FontVariation } from '@harnessio/design-system' import type { OpenapiCommitFilesRequest, RepoCommitFilesResponse, RepoFileContent, TypesPipeline } from 'services/code' import { useStrings } from 'framework/strings' import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata' import { useGetResourceContent } from 'hooks/useGetResourceContent' import MonacoSourceCodeEditor from 'components/SourceCodeEditor/MonacoSourceCodeEditor' import { PluginsPanel } from 'components/PluginsPanel/PluginsPanel' import useRunPipelineModal from 'components/RunPipelineModal/RunPipelineModal' import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner' import { useAppContext } from 'AppContext' import type { CODEProps } from 'RouteDefinitions' import { getErrorMessage } from 'utils/Utils' import { decodeGitContent } from 'utils/GitUtils' import pipelineSchemaV1 from './schema/pipeline-schema-v1.json' import pipelineSchemaV0 from './schema/pipeline-schema-v0.json' import { YamlVersion } from './Constants' import css from './AddUpdatePipeline.module.scss' const StarterPipelineV1: Record = { version: 1, stages: [ { type: 'ci', spec: { steps: [ { type: 'script', spec: { run: 'echo hello world' } } ] } } ] } const StarterPipelineV0: Record = { kind: 'pipeline', type: 'docker', name: 'default', steps: [ { name: 'test', image: 'alpine', commands: ['echo hello world'] } ] } enum PipelineSaveAndRunAction { SAVE, RUN, SAVE_AND_RUN } interface PipelineSaveAndRunOption { title: string action: PipelineSaveAndRunAction } const AddUpdatePipeline = (): JSX.Element => { const version = YamlVersion.V0 const { routes } = useAppContext() const { getString } = useStrings() const { pipeline } = useParams() const { repoMetadata } = useGetRepositoryMetadata() const { showError, showSuccess } = useToaster() const [pipelineAsObj, setPipelineAsObj] = useState>( version === YamlVersion.V0 ? StarterPipelineV0 : StarterPipelineV1 ) const [pipelineAsYAML, setPipelineAsYaml] = useState('') const { openModal: openRunPipelineModal } = useRunPipelineModal() const repoPath = useMemo(() => repoMetadata?.path || '', [repoMetadata]) const [isExistingPipeline, setIsExistingPipeline] = useState(false) const [isDirty, setIsDirty] = useState(false) const pipelineSaveOption: PipelineSaveAndRunOption = { title: getString('save'), action: PipelineSaveAndRunAction.SAVE } const pipelineRunOption: PipelineSaveAndRunOption = { title: getString('run'), action: PipelineSaveAndRunAction.RUN } const pipelineSaveAndRunOption: PipelineSaveAndRunOption = { title: getString('pipelines.saveAndRun'), action: PipelineSaveAndRunAction.SAVE_AND_RUN } const pipelineSaveAndRunOptions: PipelineSaveAndRunOption[] = [pipelineSaveAndRunOption, pipelineSaveOption] const [selectedOption, setSelectedOption] = useState() const { mutate, loading } = useMutate({ verb: 'POST', path: `/api/v1/repos/${repoPath}/+/commits` }) // Fetch pipeline metadata to fetch pipeline YAML file content const { data: pipelineData, loading: fetchingPipeline } = useGet({ path: `/api/v1/repos/${repoPath}/+/pipelines/${pipeline}`, lazy: !repoMetadata }) const { data: pipelineYAMLFileContent, loading: fetchingPipelineYAMLFileContent, refetch: fetchPipelineYAMLFileContent } = useGetResourceContent({ repoMetadata, gitRef: pipelineData?.default_branch || '', resourcePath: pipelineData?.config_path || '' }) const originalPipelineYAMLFileContent = useMemo( (): string => decodeGitContent((pipelineYAMLFileContent?.content as RepoFileContent)?.data), [pipelineYAMLFileContent?.content] ) // check if file already exists and has some content useEffect(() => { setIsExistingPipeline(!isEmpty(originalPipelineYAMLFileContent) && !isUndefined(originalPipelineYAMLFileContent)) }, [originalPipelineYAMLFileContent]) // load initial content on the editor useEffect(() => { if (isExistingPipeline) { setPipelineAsYaml(originalPipelineYAMLFileContent) } else { // load with starter pipeline try { setPipelineAsYaml(stringify(pipelineAsObj)) } catch (ex) { // ignore exception } } }, [isExistingPipeline, originalPipelineYAMLFileContent, pipelineAsObj]) // find if editor content was modified useEffect(() => { setIsDirty(originalPipelineYAMLFileContent !== pipelineAsYAML) }, [originalPipelineYAMLFileContent, pipelineAsYAML]) // set initial CTA title useEffect(() => { setSelectedOption(isDirty ? pipelineSaveAndRunOption : pipelineRunOption) }, [isDirty]) const handleSaveAndRun = (option: PipelineSaveAndRunOption): void => { if ([PipelineSaveAndRunAction.SAVE_AND_RUN, PipelineSaveAndRunAction.SAVE].includes(option?.action)) { try { const data: OpenapiCommitFilesRequest = { actions: [ { action: isExistingPipeline ? 'UPDATE' : 'CREATE', path: pipelineData?.config_path, payload: pipelineAsYAML, sha: isExistingPipeline ? pipelineYAMLFileContent?.sha : '' } ], branch: pipelineData?.default_branch || '', title: `${isExistingPipeline ? getString('updated') : getString('created')} pipeline ${pipeline}`, message: '' } mutate(data) .then(() => { fetchPipelineYAMLFileContent() showSuccess(getString(isExistingPipeline ? 'pipelines.updated' : 'pipelines.created')) if (option?.action === PipelineSaveAndRunAction.SAVE_AND_RUN && repoMetadata && pipeline) { openRunPipelineModal({ repoMetadata, pipeline }) } setSelectedOption(pipelineRunOption) }) .catch(error => { showError(getErrorMessage(error), 0, 'pipelines.failedToSavePipeline') }) } catch (exception) { showError(getErrorMessage(exception), 0, 'pipelines.failedToSavePipeline') } } } const updatePipeline = (payload: Record): Record => { const pipelineAsObjClone = { ...pipelineAsObj } const stepInsertPath = version === YamlVersion.V0 ? 'steps' : '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 } const addUpdatePluginToPipelineYAML = (_isUpdate: boolean, pluginFormData: Record): void => { try { const updatedPipelineAsObj = updatePipeline(pluginFormData) setPipelineAsObj(updatedPipelineAsObj) setPipelineAsYaml(stringify(updatedPipelineAsObj)) } catch (ex) {} } const renderCTA = useCallback(() => { switch (selectedOption?.action) { case PipelineSaveAndRunAction.RUN: return (