diff --git a/web/src/RouteDefinitions.ts b/web/src/RouteDefinitions.ts index 1a7000138..157d40288 100644 --- a/web/src/RouteDefinitions.ts +++ b/web/src/RouteDefinitions.ts @@ -47,6 +47,7 @@ export interface CODERoutes { toCODESpaceSettings: (args: Required>) => string toCODEPipelines: (args: Required>) => string toCODEPipelineEdit: (args: Required>) => string + toCODEPipelineSettings: (args: Required>) => string toCODESecrets: (args: Required>) => string toCODEGlobalSettings: () => string @@ -98,6 +99,7 @@ export const routes: CODERoutes = { toCODESpaceSettings: ({ space }) => `/settings/${space}`, toCODEPipelines: ({ repoPath }) => `/${repoPath}/pipelines`, toCODEPipelineEdit: ({ repoPath, pipeline }) => `/${repoPath}/pipelines/${pipeline}/edit`, + toCODEPipelineSettings: ({ repoPath, pipeline }) => `/${repoPath}/pipelines/${pipeline}/triggers`, toCODESecrets: ({ space }) => `/secrets/${space}`, toCODEGlobalSettings: () => '/settings', diff --git a/web/src/RouteDestinations.tsx b/web/src/RouteDestinations.tsx index 60ec8dd21..e6bb9b39c 100644 --- a/web/src/RouteDestinations.tsx +++ b/web/src/RouteDestinations.tsx @@ -33,6 +33,7 @@ import Secret from 'pages/Secret/Secret' import Search from 'pages/Search/Search' import AddUpdatePipeline from 'pages/AddUpdatePipeline/AddUpdatePipeline' import { useAppContext } from 'AppContext' +import PipelineSettings from 'components/PipelineSettings/PipelineSettings' export const RouteDestinations: React.FC = React.memo(function RouteDestinations() { const { getString } = useStrings() @@ -194,6 +195,14 @@ export const RouteDestinations: React.FC = React.memo(function RouteDestinations )} + {standalone && ( + + + + + + )} + {standalone && ( diff --git a/web/src/components/PipelineSettings/PipelineSettings.module.scss b/web/src/components/PipelineSettings/PipelineSettings.module.scss new file mode 100644 index 000000000..e0a7e5153 --- /dev/null +++ b/web/src/components/PipelineSettings/PipelineSettings.module.scss @@ -0,0 +1,28 @@ +.main { + min-height: var(--page-height); + background-color: var(--primary-bg) !important; + + .layout { + align-items: center; + } +} + +.generalContainer { + width: 100%; + background: var(--grey-0) !important; + box-shadow: 0px 0px 1px rgba(40, 41, 61, 0.08), 0px 0.5px 2px rgba(96, 97, 112, 0.16); + border-radius: 4px; +} + +.yellowContainer { + background: var(--yellow-100) !important; + border-radius: 4px !important; +} + +.textContainer { + margin: 0 !important; +} + +.withError { + display: grid; +} diff --git a/web/src/components/PipelineSettings/PipelineSettings.module.scss.d.ts b/web/src/components/PipelineSettings/PipelineSettings.module.scss.d.ts new file mode 100644 index 000000000..d00c74915 --- /dev/null +++ b/web/src/components/PipelineSettings/PipelineSettings.module.scss.d.ts @@ -0,0 +1,8 @@ +/* eslint-disable */ +// This is an auto-generated file +export declare const generalContainer: string +export declare const layout: string +export declare const main: string +export declare const textContainer: string +export declare const withError: string +export declare const yellowContainer: string diff --git a/web/src/components/PipelineSettings/PipelineSettings.tsx b/web/src/components/PipelineSettings/PipelineSettings.tsx new file mode 100644 index 000000000..90252615c --- /dev/null +++ b/web/src/components/PipelineSettings/PipelineSettings.tsx @@ -0,0 +1,247 @@ +import { + Button, + ButtonVariation, + Container, + FormInput, + Formik, + FormikForm, + Layout, + PageBody, + Text, + useToaster +} from '@harnessio/uicore' +import React from 'react' +import { useHistory, useParams } from 'react-router-dom' +import { Color, Intent } from '@harnessio/design-system' +import { useGet, useMutate } from 'restful-react' +import cx from 'classnames' +import * as yup from 'yup' +import PipelineSettingsPageHeader from 'components/PipelineSettingsPageHeader/PipelineSettingsPageHeader' +import { String, useStrings } from 'framework/strings' +import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata' +import { routes, type CODEProps } from 'RouteDefinitions' +import { useConfirmAct } from 'hooks/useConfirmAction' +import { getErrorMessage, voidFn } from 'utils/Utils' +import type { OpenapiUpdatePipelineRequest, TypesPipeline } from 'services/code' +import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner' +import css from './PipelineSettings.module.scss' + +export enum TabOptions { + SETTINGS = 'Settings', + TRIGGERS = 'Triggers' +} + +interface SettingsContentProps { + pipeline: string + repoPath: string + yamlPath: string +} + +interface SettingsFormData { + name: string + yamlPath: string +} + +const SettingsContent = ({ pipeline, repoPath, yamlPath }: SettingsContentProps) => { + const { getString } = useStrings() + const { mutate: updatePipeline } = useMutate({ + verb: 'PATCH', + path: `/api/v1/repos/${repoPath}/+/pipelines/${pipeline}` + }) + const { mutate: deletePipeline } = useMutate({ + verb: 'DELETE', + path: `/api/v1/repos/${repoPath}/+/pipelines/${pipeline}` + }) + const { showSuccess, showError } = useToaster() + const confirmDeletePipeline = useConfirmAct() + const history = useHistory() + + return ( + + + + initialValues={{ + name: pipeline, + yamlPath + }} + formName="pipelineSettings" + enableReinitialize={true} + validationSchema={yup.object().shape({ + name: yup + .string() + .trim() + .required(`${getString('name')} ${getString('isRequired')}`), + yamlPath: yup + .string() + .trim() + .required(`${getString('pipelines.yamlPath')} ${getString('isRequired')}`) + })} + validateOnChange + validateOnBlur + onSubmit={async formData => { + const { name, yamlPath: newYamlPath } = formData + try { + const payload: OpenapiUpdatePipelineRequest = { + config_path: newYamlPath, + uid: name + } + await updatePipeline(payload, { + pathParams: { path: `/api/v1/repos/${repoPath}/+/pipelines/${pipeline}` } + }) + history.push( + routes.toCODEPipelineSettings({ + repoPath, + pipeline: name + }) + ) + showSuccess(getString('pipelines.updatePipelineSuccess', { pipeline })) + } catch (exception) { + showError(getErrorMessage(exception), 0, 'pipelines.failedToUpdatePipeline') + } + }}> + {() => { + return ( + + + + + {getString('name')} + + } + /> + + + + {getString('pipelines.yamlPath')} + + } + /> + + + + + + + + ) +} + +const TriggersContent = () => { + return
Triggers
+} + +const PipelineSettings = () => { + const { getString } = useStrings() + + const { pipeline } = useParams() + const { repoMetadata, error, loading, refetch } = useGetRepositoryMetadata() + + const { + data: pipelineData, + error: pipelineError, + loading: pipelineLoading + } = useGet({ + path: `/api/v1/repos/${repoMetadata?.path}/+/pipelines/${pipeline}`, + lazy: !repoMetadata + }) + + const [selectedTab, setSelectedTab] = React.useState(TabOptions.SETTINGS) + + return ( + + + + + {selectedTab === TabOptions.SETTINGS && ( + + )} + {selectedTab === TabOptions.TRIGGERS && } + + + ) +} + +export default PipelineSettings diff --git a/web/src/components/PipelineSettingsPageHeader/PipelineSettingsPageHeader.module.scss b/web/src/components/PipelineSettingsPageHeader/PipelineSettingsPageHeader.module.scss new file mode 100644 index 000000000..23aa7cd55 --- /dev/null +++ b/web/src/components/PipelineSettingsPageHeader/PipelineSettingsPageHeader.module.scss @@ -0,0 +1,31 @@ +.pageHeader { + height: auto !important; + flex-direction: column !important; + align-items: flex-start !important; + gap: var(--spacing-small) !important; + padding-bottom: 0 !important; +} + +.breadcrumb { + align-items: center; + + a { + font-size: var(--font-size-small); + color: var(--primary-7); + } +} + +.tabs { + display: flex !important; + gap: 1rem !important; + .tab { + padding: 0.5rem 1rem !important; + cursor: pointer !important; + color: var(--grey-700) !important; + &.active { + color: var(--grey-900) !important; + font-weight: 600 !important; + border-bottom: 2px solid var(--primary-7) !important; // example of an active tab indicator + } + } +} diff --git a/web/src/components/PipelineSettingsPageHeader/PipelineSettingsPageHeader.module.scss.d.ts b/web/src/components/PipelineSettingsPageHeader/PipelineSettingsPageHeader.module.scss.d.ts new file mode 100644 index 000000000..a952ffaf5 --- /dev/null +++ b/web/src/components/PipelineSettingsPageHeader/PipelineSettingsPageHeader.module.scss.d.ts @@ -0,0 +1,7 @@ +/* eslint-disable */ +// This is an auto-generated file +export declare const active: string +export declare const breadcrumb: string +export declare const pageHeader: string +export declare const tab: string +export declare const tabs: string diff --git a/web/src/components/PipelineSettingsPageHeader/PipelineSettingsPageHeader.tsx b/web/src/components/PipelineSettingsPageHeader/PipelineSettingsPageHeader.tsx new file mode 100644 index 000000000..fe055d0b6 --- /dev/null +++ b/web/src/components/PipelineSettingsPageHeader/PipelineSettingsPageHeader.tsx @@ -0,0 +1,82 @@ +import React, { Fragment } from 'react' +import { Layout, PageHeader, Container } from '@harnessio/uicore' +import { Icon } from '@harnessio/icons' +import { Color } from '@harnessio/design-system' +import { Link, useParams } from 'react-router-dom' +import { useStrings } from 'framework/strings' +import { useAppContext } from 'AppContext' +import { useGetSpaceParam } from 'hooks/useGetSpaceParam' +import type { CODEProps } from 'RouteDefinitions' +import type { GitInfoProps } from 'utils/GitUtils' +import { TabOptions } from 'components/PipelineSettings/PipelineSettings' +import css from './PipelineSettingsPageHeader.module.scss' + +interface BreadcrumbLink { + label: string + url: string +} + +interface PipelineSettingsPageHeaderProps extends Optional, 'repoMetadata'> { + title: string | JSX.Element + dataTooltipId: string + extraBreadcrumbLinks?: BreadcrumbLink[] + selectedTab: TabOptions + setSelectedTab: (tab: TabOptions) => void +} + +const PipelineSettingsPageHeader = ({ + repoMetadata, + title, + extraBreadcrumbLinks = [], + selectedTab, + setSelectedTab +}: PipelineSettingsPageHeaderProps) => { + const { gitRef } = useParams() + const { getString } = useStrings() + const space = useGetSpaceParam() + const { routes } = useAppContext() + + if (!repoMetadata) { + return null + } + + return ( + + {getString('repositories')} + + + {repoMetadata.uid} + + {extraBreadcrumbLinks.map(link => ( + + + {link.label} + + ))} + + } + content={ + + {Object.values(TabOptions).map(tabOption => ( +
setSelectedTab(tabOption)}> + {tabOption} +
+ ))} +
+ } + /> + ) +} + +export default PipelineSettingsPageHeader diff --git a/web/src/framework/strings/stringTypes.ts b/web/src/framework/strings/stringTypes.ts index 0b3ac30a1..fde04567e 100644 --- a/web/src/framework/strings/stringTypes.ts +++ b/web/src/framework/strings/stringTypes.ts @@ -327,12 +327,18 @@ export interface StringsMap { pending: string 'pipelines.createNewPipeline': string 'pipelines.created': string + 'pipelines.deletePipelineButton': string + 'pipelines.deletePipelineConfirm': string + 'pipelines.deletePipelineError': string + 'pipelines.deletePipelineSuccess': string + 'pipelines.deletePipelineWarning': string 'pipelines.editPipeline': string 'pipelines.enterPipelineName': string 'pipelines.enterYAMLPath': string 'pipelines.executionCouldNotStart': string 'pipelines.executionStarted': string 'pipelines.failedToCreatePipeline': string + 'pipelines.failedToUpdatePipeline': string 'pipelines.lastExecution': string 'pipelines.name': string 'pipelines.newPipelineButton': string @@ -340,6 +346,7 @@ export interface StringsMap { 'pipelines.run': string 'pipelines.saveAndRun': string 'pipelines.time': string + 'pipelines.updatePipelineSuccess': string 'pipelines.updated': string 'pipelines.yamlPath': string 'plugins.addAPlugin': string diff --git a/web/src/i18n/strings.en.yaml b/web/src/i18n/strings.en.yaml index eb8109dc0..4857d1112 100644 --- a/web/src/i18n/strings.en.yaml +++ b/web/src/i18n/strings.en.yaml @@ -644,6 +644,13 @@ pipelines: updated: Pipeline updated successfully executionStarted: Pipeline execution started successfully executionCouldNotStart: Failure while starting Pipeline execution + deletePipelineWarning: This will permanently delete the {{pipeline}} pipeline and all executions. + deletePipelineButton: Delete pipeline + deletePipelineConfirm: Are you sure you want to delete the pipeline {{pipeline}}? You can't undo this action. + deletePipelineSuccess: Pipeline {{pipeline}} deleted. + deletePipelineError: Failed to delete Pipeline. Please try again. + updatePipelineSuccess: Pipeline {{pipeline}} updated. + failedToUpdatePipeline: Failed to update Pipeline. Please try again. executions: noData: There are no executions newExecutionButton: Run Pipeline diff --git a/web/src/layouts/menu/DefaultMenu.tsx b/web/src/layouts/menu/DefaultMenu.tsx index d5b09e6d7..91eedac6f 100644 --- a/web/src/layouts/menu/DefaultMenu.tsx +++ b/web/src/layouts/menu/DefaultMenu.tsx @@ -127,6 +127,15 @@ export const DefaultMenu: React.FC = () => { /> )} + + {!standalone && ( { { ) }} /> + { + e.stopPropagation() + history.push( + routes.toCODEPipelineSettings({ repoPath: repoMetadata?.path || '', pipeline: uid as string }) + ) + }} + /> ) } } ], - [getString, repoMetadata?.path, routes, searchTerm] + [getString, history, repoMetadata?.path, routes, searchTerm] ) return ( @@ -251,7 +261,7 @@ const PipelineList = () => { /> pipelines?.length === 0 && searchTerm === undefined,