pipeline settings WIP

This commit is contained in:
Dan Wilson 2023-09-13 18:16:11 +01:00
parent 86bc727825
commit 6c9eff97cc
13 changed files with 450 additions and 3 deletions

View File

@ -47,6 +47,7 @@ export interface CODERoutes {
toCODESpaceSettings: (args: Required<Pick<CODEProps, 'space'>>) => string
toCODEPipelines: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
toCODEPipelineEdit: (args: Required<Pick<CODEProps, 'repoPath' | 'pipeline'>>) => string
toCODEPipelineSettings: (args: Required<Pick<CODEProps, 'repoPath' | 'pipeline'>>) => string
toCODESecrets: (args: Required<Pick<CODEProps, 'space'>>) => 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',

View File

@ -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
</Route>
)}
{standalone && (
<Route path={routes.toCODEPipelineSettings({ repoPath, pipeline: pathProps.pipeline })} exact>
<LayoutWithSideNav title={getString('pageTitle.pipelines')}>
<PipelineSettings />
</LayoutWithSideNav>
</Route>
)}
{standalone && (
<Route path={routes.toCODEPipelines({ repoPath })} exact>
<LayoutWithSideNav title={getString('pageTitle.pipelines')}>

View File

@ -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;
}

View File

@ -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

View File

@ -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<TypesPipeline>({
verb: 'PATCH',
path: `/api/v1/repos/${repoPath}/+/pipelines/${pipeline}`
})
const { mutate: deletePipeline } = useMutate<TypesPipeline>({
verb: 'DELETE',
path: `/api/v1/repos/${repoPath}/+/pipelines/${pipeline}`
})
const { showSuccess, showError } = useToaster()
const confirmDeletePipeline = useConfirmAct()
const history = useHistory()
return (
<Layout.Vertical padding={'medium'} spacing={'medium'}>
<Container padding={'large'} className={css.generalContainer}>
<Formik<SettingsFormData>
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 (
<FormikForm>
<Layout.Vertical spacing={'large'}>
<Layout.Horizontal spacing={'large'} flex={{ alignItems: 'center', justifyContent: 'flex-start' }}>
<FormInput.Text
name="name"
className={css.textContainer}
label={
<Text color={Color.GREY_800} font={{ size: 'small' }}>
{getString('name')}
</Text>
}
/>
</Layout.Horizontal>
<Layout.Horizontal spacing={'large'} flex={{ alignItems: 'center', justifyContent: 'flex-start' }}>
<FormInput.Text
name="yamlPath"
className={css.textContainer}
label={
<Text color={Color.GREY_800} font={{ size: 'small' }}>
{getString('pipelines.yamlPath')}
</Text>
}
/>
</Layout.Horizontal>
<Layout.Horizontal spacing={'large'}>
<Button intent={Intent.PRIMARY} type="submit" text={getString('save')} />
<Button variation={ButtonVariation.TERTIARY} type="reset" text={getString('cancel')} />
</Layout.Horizontal>
</Layout.Vertical>
</FormikForm>
)
}}
</Formik>
</Container>
<Container padding={'large'} className={css.generalContainer}>
<Layout.Vertical>
<Text icon="main-trash" color={Color.GREY_600} font={{ size: 'normal' }}>
{getString('dangerDeleteRepo')}
</Text>
<Layout.Horizontal padding={{ top: 'medium', left: 'medium' }} flex={{ justifyContent: 'space-between' }}>
<Container intent="warning" padding={'small'} className={css.yellowContainer}>
<Text
icon="main-issue"
iconProps={{ size: 18, color: Color.ORANGE_700, margin: { right: 'small' } }}
color={Color.WARNING}>
{getString('pipelines.deletePipelineWarning', {
pipeline
})}
</Text>
</Container>
<Button
margin={{ right: 'medium' }}
intent={Intent.DANGER}
onClick={() => {
confirmDeletePipeline({
title: getString('pipelines.deletePipelineButton'),
confirmText: getString('delete'),
intent: Intent.DANGER,
message: <String useRichText stringID="pipelines.deletePipelineConfirm" vars={{ pipeline }} />,
action: async () => {
try {
await deletePipeline(null)
history.push(
routes.toCODEPipelines({
repoPath
})
)
showSuccess(getString('pipelines.deletePipelineSuccess', { pipeline }))
} catch (e) {
showError(getString('pipelines.deletePipelineError'))
}
}
})
}}
variation={ButtonVariation.PRIMARY}
text={getString('pipelines.deletePipelineButton')}></Button>
</Layout.Horizontal>
</Layout.Vertical>
</Container>
</Layout.Vertical>
)
}
const TriggersContent = () => {
return <div>Triggers</div>
}
const PipelineSettings = () => {
const { getString } = useStrings()
const { pipeline } = useParams<CODEProps>()
const { repoMetadata, error, loading, refetch } = useGetRepositoryMetadata()
const {
data: pipelineData,
error: pipelineError,
loading: pipelineLoading
} = useGet<TypesPipeline>({
path: `/api/v1/repos/${repoMetadata?.path}/+/pipelines/${pipeline}`,
lazy: !repoMetadata
})
const [selectedTab, setSelectedTab] = React.useState<TabOptions>(TabOptions.SETTINGS)
return (
<Container className={css.main}>
<PipelineSettingsPageHeader
repoMetadata={repoMetadata}
title={`${pipeline} settings`}
dataTooltipId="pipelineSettings"
selectedTab={selectedTab}
setSelectedTab={setSelectedTab}
extraBreadcrumbLinks={
repoMetadata && [
{
label: getString('pageTitle.pipelines'),
url: routes.toCODEPipelines({ repoPath: repoMetadata.path as string })
},
{
label: pipeline as string,
url: routes.toCODEExecutions({ repoPath: repoMetadata.path as string, pipeline: pipeline as string })
}
]
}
/>
<PageBody
className={cx({ [css.withError]: !!error })}
error={error ? getErrorMessage(error || pipelineError) : null}
retryOnError={voidFn(refetch)}>
<LoadingSpinner visible={loading || pipelineLoading} withBorder={!!pipeline} />
{selectedTab === TabOptions.SETTINGS && (
<SettingsContent
pipeline={pipeline as string}
repoPath={repoMetadata?.path as string}
yamlPath={pipelineData?.config_path as string}
/>
)}
{selectedTab === TabOptions.TRIGGERS && <TriggersContent />}
</PageBody>
</Container>
)
}
export default PipelineSettings

View File

@ -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
}
}
}

View File

@ -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

View File

@ -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<Pick<GitInfoProps, 'repoMetadata'>, '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<CODEProps>()
const { getString } = useStrings()
const space = useGetSpaceParam()
const { routes } = useAppContext()
if (!repoMetadata) {
return null
}
return (
<PageHeader
className={css.pageHeader}
title={title}
breadcrumbs={
<Layout.Horizontal
spacing="small"
className={css.breadcrumb}
padding={{ bottom: 0 }}
margin={{ bottom: 'small' }}>
<Link to={routes.toCODERepositories({ space })}>{getString('repositories')}</Link>
<Icon name="main-chevron-right" size={8} color={Color.GREY_500} />
<Link to={routes.toCODERepository({ repoPath: repoMetadata.path as string, gitRef })}>
{repoMetadata.uid}
</Link>
{extraBreadcrumbLinks.map(link => (
<Fragment key={link.url}>
<Icon name="main-chevron-right" size={8} color={Color.GREY_500} />
<Link to={link.url}>{link.label}</Link>
</Fragment>
))}
</Layout.Horizontal>
}
content={
<Container className={css.tabs}>
{Object.values(TabOptions).map(tabOption => (
<div
key={tabOption}
className={`${css.tab} ${selectedTab === tabOption ? css.active : ''}`}
onClick={() => setSelectedTab(tabOption)}>
{tabOption}
</div>
))}
</Container>
}
/>
)
}
export default PipelineSettingsPageHeader

View File

@ -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

View File

@ -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 <strong>{{pipeline}}</strong>? 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

View File

@ -127,6 +127,15 @@ export const DefaultMenu: React.FC = () => {
/>
)}
<NavMenuItem
data-code-repo-section="settings"
isSubLink
label={getString('settings')}
to={routes.toCODESettings({
repoPath
})}
/>
{!standalone && (
<NavMenuItem
data-code-repo-section="search"

View File

@ -65,7 +65,7 @@ const Execution = () => {
<Container className={css.main}>
<ExecutionPageHeader
repoMetadata={repoMetadata}
title={execution?.title as string}
title={pipeline as string}
dataTooltipId="repositoryExecution"
extraBreadcrumbLinks={
repoMetadata && [

View File

@ -233,13 +233,23 @@ const PipelineList = () => {
)
}}
/>
<MenuItem
icon="settings"
text={getString('settings')}
onClick={e => {
e.stopPropagation()
history.push(
routes.toCODEPipelineSettings({ repoPath: repoMetadata?.path || '', pipeline: uid as string })
)
}}
/>
</Menu>
</Popover>
)
}
}
],
[getString, repoMetadata?.path, routes, searchTerm]
[getString, history, repoMetadata?.path, routes, searchTerm]
)
return (
@ -251,7 +261,7 @@ const PipelineList = () => {
/>
<PageBody
className={cx({ [css.withError]: !!error })}
error={error ? getErrorMessage(error || pipelinesError) : null}
error={error || pipelinesError ? getErrorMessage(error || pipelinesError) : null}
retryOnError={voidFn(refetch)}
noData={{
when: () => pipelines?.length === 0 && searchTerm === undefined,