feat: [AH-882]: Implement create webhook modal (#3288)

* feat: [AH-882]: fix circular dependancy check plugin for ar
* feat: [AH-882]: fix PR comments
* feat: [AH-882]: Add Generic URL validation
* feat: [AH-882]: implement support for extra headers in create webhook form
* feat: [AH-882]: bufixes in webhook form content
* [AH-882]: Implement create webhook modal
pull/3616/head
Shivanand Sonnad 2025-01-17 13:40:00 +00:00 committed by Harness
parent d6ea2b8de6
commit 3c5a2a4ab0
27 changed files with 813 additions and 13 deletions

View File

@ -14,7 +14,18 @@
* limitations under the License.
*/
import type { FormikProps } from 'formik'
import type { DataTooltipInterface } from '@harnessio/uicore'
import type { FormikContextType, FormikProps } from 'formik'
export interface FormikExtended<T> extends FormikContextType<T> {
disabled?: boolean
formName: string
}
export interface FormikContextProps<T> {
formik?: FormikExtended<T>
tooltipProps?: DataTooltipInterface
}
export enum Parent {
OSS = 'OSS',

View File

@ -0,0 +1,82 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react'
import { get } from 'lodash-es'
import { connect } from 'formik'
import { FormGroup, IFormGroupProps, Intent } from '@blueprintjs/core'
import { Checkbox, FormError, getFormFieldLabel, errorCheck } from '@harnessio/uicore'
import type { FormikContextProps } from '@ar/common/types'
interface CheckboxItem {
label: string
value: string
disabled?: boolean
tooltipId?: string
}
export interface CheckboxGroupProps extends Omit<IFormGroupProps, 'labelFor'> {
name: string
items: CheckboxItem[]
}
function CheckboxGroup(props: CheckboxGroupProps & FormikContextProps<any>) {
const { formik, name } = props
const hasError = errorCheck(name, formik)
const {
intent = hasError ? Intent.DANGER : Intent.NONE,
helperText = hasError ? <FormError name={name} errorMessage={get(formik?.errors, name)} /> : null,
disabled = formik?.disabled,
items = [],
label,
...rest
} = props
const formValue: string[] = get(formik?.values, name, [])
const handleChange = (val: string, checked: boolean) => {
const newValue = checked ? [...formValue, val] : formValue.filter(v => v !== val)
formik?.setFieldValue(name, newValue)
}
return (
<FormGroup
label={getFormFieldLabel(label, name, props)}
labelFor={name}
helperText={helperText}
intent={intent}
disabled={disabled}
{...rest}>
{items.map(item => {
return (
<Checkbox
key={item.value}
value={item.value}
disabled={disabled}
checked={formValue?.includes(item.value)}
onChange={e => {
handleChange(item.value, e.currentTarget.checked)
}}
label={item.label}
/>
)
})}
</FormGroup>
)
}
export default connect(CheckboxGroup)

View File

@ -58,6 +58,7 @@ const prodConfig = {
}),
new CircularDependencyPlugin({
exclude: /node_modules/,
include: /src\/ar/,
failOnError: true
}),
new RetryChunkLoadPlugin({

View File

@ -28,6 +28,7 @@ export const DEFAULT_DATE_TIME_FORMAT = `${DEFAULT_DATE_FORMAT} ${DEFAULT_TIME_F
export const REPO_KEY_REGEX = /^[a-z0-9]+(?:[._-][a-z0-9]+)*$/
export const URL_REGEX = /^https:\/\/([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,6}(:[0-9]{1,5})?(\/[^\s]*)?$/
export const GENERIC_URL_REGEX = /^(https?):\/\/([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,6}(:[0-9]{1,5})?(\/[^\s]*)?$/
export enum PreferenceScope {
USER = 'USER',

View File

@ -24,6 +24,7 @@ export default function getARRouteDefinitions(routeParams: Record<string, string
toARRepositories: () => '/',
toARRepositoryDetails: params => `/${params?.repositoryIdentifier}`,
toARRepositoryDetailsTab: params => `/${params?.repositoryIdentifier}/${params?.tab}`,
toARRepositoryWebhookDetails: params => `/${params?.repositoryIdentifier}/webhooks/${params?.webhookIdentifier}`,
toARArtifacts: () => `/${routeParams?.repositoryIdentifier}?tab=packages`,
toARArtifactDetails: params => `/${params?.repositoryIdentifier}/artifacts/${params?.artifactIdentifier}`,
toARVersionDetails: params =>

View File

@ -15,14 +15,38 @@
*/
import React from 'react'
import { Button, ButtonVariation } from '@harnessio/uicore'
import { useHistory, useParams } from 'react-router-dom'
import type { Webhook } from '@harnessio/react-har-service-client'
import { Button, ButtonVariation, useToggleOpen } from '@harnessio/uicore'
import { useRoutes } from '@ar/hooks'
import { useStrings } from '@ar/frameworks/strings'
import type { RepositoryDetailsTabPathParams } from '@ar/routes/types'
import CreateWebhookModal from './CreateWebhookModal'
export default function CreateWebhookButton() {
const { isOpen, close, open } = useToggleOpen()
const { getString } = useStrings()
const { repositoryIdentifier } = useParams<RepositoryDetailsTabPathParams>()
const routes = useRoutes()
const history = useHistory()
const handleAfterCreateWebhook = (data: Webhook) => {
history.push(
routes.toARRepositoryWebhookDetails({
repositoryIdentifier,
webhookIdentifier: data.identifier
})
)
}
return (
<Button variation={ButtonVariation.PRIMARY} icon="plus" iconProps={{ size: 10 }}>
{getString('webhookList.newWebhook')}
</Button>
<>
<Button variation={ButtonVariation.PRIMARY} icon="plus" iconProps={{ size: 10 }} onClick={open}>
{getString('webhookList.newWebhook')}
</Button>
<CreateWebhookModal isOpen={isOpen} onClose={close} onSubmit={handleAfterCreateWebhook} />
</>
)
}

View File

@ -0,0 +1,84 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useRef, useState } from 'react'
import type { FormikProps } from 'formik'
import type { IDialogProps } from '@blueprintjs/core'
import { createWebhook, Webhook, WebhookRequest } from '@harnessio/react-har-service-client'
import { Button, ButtonVariation, Layout, ModalDialog, useToaster } from '@harnessio/uicore'
import { useGetSpaceRef } from '@ar/hooks'
import { useStrings } from '@ar/frameworks/strings'
import { getErrorMessage } from 'utils/Utils'
import WebhookForm from '../Forms/WebhookForm'
interface CreateWebhookModalProps extends IDialogProps {
onSubmit: (res: Webhook) => void
}
export default function CreateWebhookModal(props: CreateWebhookModalProps) {
const { onSubmit, isOpen, onClose, title } = props
const [isLoading, setLoading] = useState(false)
const { getString } = useStrings()
const registryRef = useGetSpaceRef()
const { showError, clear, showSuccess } = useToaster()
const formRef = useRef<FormikProps<WebhookRequest>>(null)
const handleSubmit = () => {
formRef.current?.submitForm()
}
const handleCreateWebhook = async (formData: WebhookRequest) => {
try {
setLoading(true)
const response = await createWebhook({
registry_ref: registryRef,
body: formData
})
showSuccess(getString('webhookList.webhookCreated'))
onSubmit(response.content.data)
} catch (e) {
clear()
showError(getErrorMessage(e))
} finally {
setLoading(false)
}
}
return (
<ModalDialog
title={title ?? getString('webhookList.newWebhook')}
onClose={onClose}
isOpen={isOpen}
showOverlay={isLoading}
footer={
<Layout.Horizontal spacing="small">
<Button variation={ButtonVariation.PRIMARY} onClick={handleSubmit}>
{getString('add')}
</Button>
<Button variation={ButtonVariation.SECONDARY} onClick={() => onClose?.()}>
{getString('cancel')}
</Button>
</Layout.Horizontal>
}>
<WebhookForm onSubmit={handleCreateWebhook} ref={formRef} />
</ModalDialog>
)
}

View File

@ -0,0 +1,49 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react'
import { FieldArray, FormikProps } from 'formik'
import { Layout } from '@harnessio/uicore'
import type { WebhookRequestUI } from '../types'
import ExtraHeadersList from './ExtraHeadersList'
interface ExtraHeadersFormContentProps {
formikProps: FormikProps<WebhookRequestUI>
disabled?: boolean
}
export default function ExtraHeadersFormContent(props: ExtraHeadersFormContentProps) {
const { formikProps, disabled } = props
return (
<Layout.Vertical spacing="small">
<FieldArray
name="extraHeaders"
render={({ push, remove }) => {
return (
<ExtraHeadersList
onAdd={push}
onRemove={remove}
name="extraHeaders"
formikProps={formikProps}
disabled={disabled}
/>
)
}}
/>
</Layout.Vertical>
)
}

View File

@ -0,0 +1,68 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react'
import type { FormikProps } from 'formik'
import type { ExtraHeader } from '@harnessio/react-har-service-client'
import { Button, ButtonSize, ButtonVariation, FormInput, Layout } from '@harnessio/uicore'
import { useStrings } from '@ar/frameworks/strings'
import type { WebhookRequestUI } from '../types'
interface ExtraHeadersListProps {
onAdd: (item: ExtraHeader) => void
onRemove: (index: number) => void
name: keyof WebhookRequestUI
formikProps: FormikProps<WebhookRequestUI>
disabled?: boolean
}
export default function ExtraHeadersList(props: ExtraHeadersListProps) {
const { onAdd, onRemove, name, formikProps, disabled } = props
const list = formikProps.values[name] as ExtraHeader[]
const { getString } = useStrings()
return (
<Layout.Vertical flex={{ alignItems: 'flex-start' }}>
{list?.map((_each: ExtraHeader, index: number) => (
<Layout.Horizontal key={index} spacing="medium">
<FormInput.Text
inline
name={`${name}[${index}].key`}
label={getString('webhookList.formFields.extraHeader')}
placeholder={getString('webhookList.formFields.extraHeaderPlaceholder')}
disabled={disabled}
/>
<FormInput.Text
inline
name={`${name}[${index}].value`}
label={getString('webhookList.formFields.extraValue')}
placeholder={getString('webhookList.formFields.extraValuePlaceholder')}
disabled={disabled}
/>
<Button variation={ButtonVariation.ICON} icon="code-delete" onClick={() => onRemove(index)} />
</Layout.Horizontal>
))}
<Button
size={ButtonSize.SMALL}
icon="plus"
variation={ButtonVariation.LINK}
onClick={() => onAdd({ key: '', value: '' })}>
{getString('webhookList.formFields.addNewKeyValuePair')}
</Button>
</Layout.Vertical>
)
}

View File

@ -0,0 +1,27 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { type PropsWithChildren } from 'react'
import { Text, type TextProps } from '@harnessio/uicore'
import { Color, FontVariation } from '@harnessio/design-system'
export default function FormLabel(props: PropsWithChildren<TextProps>) {
return (
<Text font={{ variation: FontVariation.FORM_LABEL, weight: 'bold' }} color={Color.GREY_900} {...props}>
{props.children}
</Text>
)
}

View File

@ -0,0 +1,19 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.triggerType {
margin-bottom: var(--spacing-xsmall) !important;
}

View File

@ -0,0 +1,19 @@
/*
* Copyright 2023 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable */
// This is an auto-generated file
export declare const triggerType: string

View File

@ -0,0 +1,63 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react'
import type { FormikProps } from 'formik'
import { Container, FormInput, Layout } from '@harnessio/uicore'
import { useStrings } from '@ar/frameworks/strings'
import CheckboxGroup from '@ar/components/Form/CheckboxGroup/CheckboxGroup'
import FormLabel from './FormLabel'
import type { WebhookRequestUI } from './types'
import { TriggerLabelOptions } from './constants'
import css from './Forms.module.scss'
interface SelectTriggersProps {
formikProps: FormikProps<WebhookRequestUI>
disabled?: boolean
}
export default function SelectTriggers(props: SelectTriggersProps) {
const { formikProps, disabled } = props
const triggerType = formikProps.values.triggerType
const { getString } = useStrings()
return (
<Layout.Vertical spacing="small">
<FormLabel>{getString('webhookList.formFields.triggerLabel')}</FormLabel>
<FormInput.RadioGroup
key={triggerType}
className={css.triggerType}
disabled={disabled}
name="triggerType"
items={[
{ label: getString('webhookList.formFields.allTrigger'), value: 'all' },
{ label: getString('webhookList.formFields.customTrigger'), value: 'custom' }
]}
/>
{triggerType === 'custom' && (
<Container margin={{ left: 'large' }}>
<CheckboxGroup
disabled={disabled}
items={TriggerLabelOptions.map(each => ({ ...each, label: getString(each.label) }))}
name="triggers"
/>
</Container>
)}
</Layout.Vertical>
)
}

View File

@ -0,0 +1,84 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { forwardRef, useMemo } from 'react'
import * as Yup from 'yup'
import { Formik } from '@harnessio/uicore'
import type { WebhookRequest } from '@harnessio/react-har-service-client'
import { useAppStore } from '@ar/hooks'
import { GENERIC_URL_REGEX } from '@ar/constants'
import { setFormikRef } from '@ar/common/utils'
import { useStrings } from '@ar/frameworks/strings'
import type { FormikFowardRef } from '@ar/common/types'
import type { WebhookRequestUI } from './types'
import WebhookFormContent from './WebhookFormContent'
import { transformFormValuesToSubmitValues } from './utils'
interface CreateWebhookFormProps {
onSubmit: (values: WebhookRequest) => void
readonly?: boolean
isEdit?: boolean
}
function WebhookForm(props: CreateWebhookFormProps, formikRef: FormikFowardRef<WebhookRequestUI>) {
const { onSubmit, readonly, isEdit } = props
const { getString } = useStrings()
const { parent, scope } = useAppStore()
const initialValues: WebhookRequestUI = useMemo(() => {
return {
identifier: '',
name: '',
url: '',
triggerType: 'all',
enabled: true,
insecure: false,
extraHeaders: [{ key: '', value: '' }]
}
}, [])
const handleSubmit = (values: WebhookRequestUI) => {
const formValues = transformFormValuesToSubmitValues(values, parent, scope)
onSubmit(formValues)
}
return (
<Formik<WebhookRequestUI>
formName="webhook-form"
validationSchema={Yup.object().shape({
identifier: Yup.string().required(getString('validationMessages.identifierRequired')),
name: Yup.string().required(getString('validationMessages.nameRequired')),
url: Yup.string()
.required(getString('validationMessages.urlRequired'))
.matches(GENERIC_URL_REGEX, getString('validationMessages.genericURLPattern')),
triggers: Yup.array().when(['triggerType'], {
is: (triggerType: WebhookRequestUI['triggerType']) => triggerType === 'custom',
then: (schema: Yup.StringSchema) => schema.required(getString('validationMessages.required')),
otherwise: (schema: Yup.StringSchema) => schema.notRequired()
})
})}
onSubmit={handleSubmit}
initialValues={initialValues}>
{formik => {
setFormikRef(formikRef, formik)
return <WebhookFormContent formikProps={formik} isEdit={isEdit} readonly={readonly} />
}}
</Formik>
)
}
export default forwardRef(WebhookForm)

View File

@ -0,0 +1,107 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react'
import type { FormikProps } from 'formik'
import { Checkbox, FormikForm, FormInput, Layout } from '@harnessio/uicore'
import { useStrings } from '@ar/frameworks/strings'
import { useAppStore, useParentComponents } from '@ar/hooks'
import FormLabel from './FormLabel'
import SelectTriggers from './SelectTriggers'
import type { WebhookRequestUI } from './types'
import ExtraHeadersFormContent from './ExtraHeadersFormContent/ExtraHeadersFormContent'
interface WebhookFormContentProps {
formikProps: FormikProps<WebhookRequestUI>
readonly?: boolean
isEdit?: boolean
}
export default function WebhookFormContent(props: WebhookFormContentProps) {
const { formikProps, readonly, isEdit } = props
const { scope } = useAppStore()
const { getString } = useStrings()
const { SecretFormInput } = useParentComponents()
const values = formikProps.values
return (
<FormikForm>
<Layout.Vertical spacing="small">
<FormInput.InputWithIdentifier
inputName="name"
idName="identifier"
inputLabel={getString('webhookList.formFields.name')}
idLabel={getString('webhookList.formFields.id')}
inputGroupProps={{
placeholder: getString('enterPlaceholder', { name: getString('webhookList.formFields.name') }),
disabled: readonly
}}
isIdentifierEditable={!isEdit && !readonly}
/>
<FormInput.TextArea
name="description"
label={getString('optionalField', { name: getString('webhookList.formFields.description') })}
placeholder={getString('enterPlaceholder', { name: getString('webhookList.formFields.description') })}
disabled={readonly}
/>
<FormInput.Text
name="url"
label={getString('webhookList.formFields.url')}
placeholder={getString('enterPlaceholder', { name: getString('webhookList.formFields.url') })}
disabled={readonly}
/>
<SecretFormInput
name="secretIdentifier"
spaceIdFieldName="secretSpaceId"
label={getString('webhookList.formFields.secret')}
placeholder={getString('enterPlaceholder', { name: getString('webhookList.formFields.secret') })}
scope={scope}
disabled={readonly}
formik={formikProps}
/>
</Layout.Vertical>
<Layout.Vertical spacing="large">
<SelectTriggers formikProps={formikProps} disabled={readonly} />
<Layout.Vertical spacing="small">
<FormLabel>{getString('webhookList.formFields.SSLVerification')}</FormLabel>
<Checkbox
label={getString('webhookList.formFields.enableSSLVerification')}
checked={!values.insecure}
disabled={readonly}
onChange={e => {
formikProps.setFieldValue('insecure', !e.currentTarget.checked)
}}
/>
</Layout.Vertical>
{isEdit && (
<Layout.Vertical spacing="small">
<FormLabel>{getString('webhookList.formFields.enabled')}</FormLabel>
<FormInput.CheckBox
disabled={readonly}
label={getString('webhookList.formFields.enabled')}
name="enabled"
/>
</Layout.Vertical>
)}
<Layout.Vertical spacing="small">
<FormLabel>{getString('optionalField', { name: getString('webhookList.formFields.advanced') })}</FormLabel>
<ExtraHeadersFormContent formikProps={formikProps} disabled={readonly} />
</Layout.Vertical>
</Layout.Vertical>
</FormikForm>
)
}

View File

@ -0,0 +1,30 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Trigger } from '@harnessio/react-har-service-client'
import type { StringKeys } from '@ar/frameworks/strings'
interface TriggerLabelOption {
label: StringKeys
value: Trigger
disabled?: boolean
}
export const TriggerLabelOptions: TriggerLabelOption[] = [
{ label: 'webhookList.triggers.artifactCreation', value: 'ARTIFACT_CREATION' },
{ label: 'webhookList.triggers.artifactDeletion', value: 'ARTIFACT_DELETION' },
{ label: 'webhookList.triggers.artifactModification', value: 'ARTIFACT_MODIFICATION' }
]

View File

@ -0,0 +1,19 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { WebhookRequest } from '@harnessio/react-har-service-client'
export type WebhookRequestUI = WebhookRequest & { triggerType: 'all' | 'custom' }

View File

@ -0,0 +1,56 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import produce from 'immer'
import { get, set } from 'lodash-es'
import type { WebhookRequest } from '@harnessio/react-har-service-client'
import type { Scope } from '@ar/MFEAppTypes'
import { Parent } from '@ar/common/types'
import { getSecretSpacePath } from '@ar/pages/upstream-proxy-details/components/Forms/utils'
import type { WebhookRequestUI } from './types'
function convertSecretInputToFormFields(
formData: WebhookRequestUI,
secretField: keyof WebhookRequestUI,
secretSpacePathField: keyof WebhookRequestUI,
scope?: Scope
) {
const secret = get(formData, secretField)
set(formData, secretSpacePathField, getSecretSpacePath(get(secret, 'referenceString', ''), scope))
set(formData, secretField, get(secret, 'identifier'))
}
export function transformFormValuesToSubmitValues(
formValues: WebhookRequestUI,
parent: Parent,
scope: Scope
): WebhookRequest {
return produce(formValues, draft => {
if (draft.triggerType === 'all') {
draft.triggers = []
}
if (draft.extraHeaders?.length) {
draft.extraHeaders = draft.extraHeaders.filter(each => !!each.key && !!each.value)
}
if (parent === Parent.Enterprise) {
convertSecretInputToFormFields(draft, 'secretIdentifier', 'secretSpacePath', scope)
}
set(draft, 'triggerType', undefined)
return draft
})
}

View File

@ -23,7 +23,6 @@ import type { ListWebhooks, Webhook } from '@harnessio/react-har-service-client'
import { useStrings } from '@ar/frameworks/strings'
import { useParentHooks, useRoutes } from '@ar/hooks'
import { RepositoryDetailsTab } from '@ar/pages/repository-details/constants'
import {
WebhookActionsCell,
@ -115,11 +114,10 @@ export default function WebhookListTable(props: WebhookListTableProps): JSX.Elem
sortable
getRowClassName={() => css.tableRow}
onRowClick={rowDetails => {
// TODO: navigate to webhook details page
history.push(
routes.toARRepositoryDetailsTab({
routes.toARRepositoryWebhookDetails({
repositoryIdentifier: rowDetails.identifier,
tab: RepositoryDetailsTab.WEBHOOKS
webhookIdentifier: rowDetails.identifier
})
)
}}

View File

@ -1,4 +1,5 @@
newWebhook: New Webhook
webhookCreated: Webhook created successfully
triggers:
artifactCreation: 'Artifact Creation'
artifactDeletion: 'Artifact Deletion'
@ -8,3 +9,21 @@ table:
name: Webhook
trigger: Event
noWebhooksTitle: There are no webhooks available
formFields:
name: Name
id: Id
description: Description
url: Payload URL
secret: Secret
triggerLabel: Which events do you like to trigger this webhook?
allTrigger: Send me everything
customTrigger: Let me select individual events
SSLVerification: SSL Verification
enableSSLVerification: Enable SSL Verification
advanced: Advanced
extraHeader: Key
extraHeaderPlaceholder: Enter Header
extraValue: Value
extraValuePlaceholder: Enter Value
addNewKeyValuePair: Add New Key Value Pair
enabled: Enabled

View File

@ -20,6 +20,7 @@ import type {
RedirectPageQueryParams,
RepositoryDetailsPathParams,
RepositoryDetailsTabPathParams,
RepositoryWebhookDetailsPathParams,
VersionDetailsPathParams,
VersionDetailsTabPathParams
} from './types'
@ -34,6 +35,7 @@ export interface ARRouteDefinitionsReturn {
toARArtifactDetails: (params: ArtifactDetailsPathParams) => string
toARVersionDetails: (params: VersionDetailsPathParams) => string
toARVersionDetailsTab: (params: VersionDetailsTabPathParams) => string
toARRepositoryWebhookDetails: (params: RepositoryWebhookDetailsPathParams) => string
}
export const routeDefinitions: ARRouteDefinitionsReturn = {
@ -66,5 +68,7 @@ export const routeDefinitions: ARRouteDefinitionsReturn = {
return `/registries/${params?.repositoryIdentifier}/artifacts/${params?.artifactIdentifier}/versions/${params?.versionIdentifier}/pipelines/${params.pipelineIdentifier}/executions/${params.executionIdentifier}/${params.versionTab}`
}
return `/registries/${params?.repositoryIdentifier}/artifacts/${params?.artifactIdentifier}/versions/${params?.versionIdentifier}/${params.versionTab}`
}
},
toARRepositoryWebhookDetails: params =>
`/registries/${params?.repositoryIdentifier}/webhooks/${params?.webhookIdentifier}`
}

View File

@ -52,3 +52,7 @@ export interface RedirectPageQueryParams {
versionId?: string
versionDetailsTab?: VersionDetailsTab
}
export interface RepositoryWebhookDetailsPathParams extends RepositoryDetailsPathParams {
webhookIdentifier: string
}

View File

@ -86,14 +86,17 @@ badges:
artifactRegistry: '{{ $.repositoryList.artifactRegistry.label }}'
upstreamProxy: '{{ $.repositoryList.upstreamProxy.label }}'
validationMessages:
required: Required
repokeyRegExMessage: Registry name must start with letter and can only contain lowercase alphanumerics, _, . and -
nameRequired: Registry name is required
nameRequired: Name is required
identifierRequired: Identifier is required
cleanupPolicy:
nameRequired: Cleanup policy name is required
expireDaysRequired: Expire days is required
positiveExpireDays: Expire days must be greater than 0
urlRequired: Remote registry URL is required
urlPattern: Remote registry URL must be valid
urlRequired: URL is required
urlPattern: URL must be valid and start with https://
genericURLPattern: URL must be valid and start with http:// or https://
userNameRequired: Username is required
passwordRequired: Password is required
accessKeyRequired: Access key is required

View File

@ -228,6 +228,23 @@ export interface StringsMap {
'versionList.table.columns.size': string
'versionList.table.columns.version': string
'versionList.table.noVersionsTitle': string
'webhookList.formFields.SSLVerification': string
'webhookList.formFields.addNewKeyValuePair': string
'webhookList.formFields.advanced': string
'webhookList.formFields.allTrigger': string
'webhookList.formFields.customTrigger': string
'webhookList.formFields.description': string
'webhookList.formFields.enableSSLVerification': string
'webhookList.formFields.enabled': string
'webhookList.formFields.extraHeader': string
'webhookList.formFields.extraHeaderPlaceholder': string
'webhookList.formFields.extraValue': string
'webhookList.formFields.extraValuePlaceholder': string
'webhookList.formFields.id': string
'webhookList.formFields.name': string
'webhookList.formFields.secret': string
'webhookList.formFields.triggerLabel': string
'webhookList.formFields.url': string
'webhookList.newWebhook': string
'webhookList.table.columns.name': string
'webhookList.table.columns.trigger': string
@ -235,6 +252,7 @@ export interface StringsMap {
'webhookList.triggers.artifactCreation': string
'webhookList.triggers.artifactDeletion': string
'webhookList.triggers.artifactModification': string
'webhookList.webhookCreated': string
'actions.delete': string
'actions.edit': string
'actions.quarantine': string
@ -318,9 +336,12 @@ export interface StringsMap {
'validationMessages.cleanupPolicy.expireDaysRequired': string
'validationMessages.cleanupPolicy.nameRequired': string
'validationMessages.cleanupPolicy.positiveExpireDays': string
'validationMessages.genericURLPattern': string
'validationMessages.identifierRequired': string
'validationMessages.nameRequired': string
'validationMessages.passwordRequired': string
'validationMessages.repokeyRegExMessage': string
'validationMessages.required': string
'validationMessages.secretKeyRequired': string
'validationMessages.urlPattern': string
'validationMessages.urlRequired': string

View File

@ -23,6 +23,10 @@
margin-top: 5px !important;
}
.selectInput {
min-width: 265px;
}
&.containerWithoutLabel {
align-items: flex-start !important;

View File

@ -19,3 +19,4 @@
export declare const container: string
export declare const containerWithoutLabel: string
export declare const createNewBtn: string
export declare const selectInput: string

View File

@ -81,6 +81,7 @@ export default function SecretFormInput(props: SecretFormInputProps) {
spacing="small"
flex={{ justifyContent: 'flex-start', alignItems: 'center' }}>
<FormInput.Select
className={css.selectInput}
name={name}
label={label}
disabled={disabled}