feat: [AH-801]: Support ECR source in upstream proxy flow (#3177)

* feat: [AH-801] handle accessKeyType for gitness
* feat: [AH-801] update key for gitness
* feat: [AH-801]: support description and labels in upstream porxy create and update form
* feat: [AH-801]: fix breaking css from gitness
* feat: [AH-801]: implement MultiTypeSecretInput in HAR
* feat: [AH-801]: support ecr source in opensource
* feat: [AH-801]: Support ECR source in upstream proxy flow
BT-10437
Shivanand Sonnad 2025-01-06 08:03:44 +00:00 committed by Harness
parent 016a0ce2f5
commit f4dfe93677
35 changed files with 768 additions and 270 deletions

View File

@ -51,7 +51,7 @@
"@codemirror/view": "^6.9.6",
"@harnessio/design-system": "^2.1.1",
"@harnessio/icons": "^2.1.9",
"@harnessio/react-har-service-client": "^0.3.0",
"@harnessio/react-har-service-client": "^0.4.0",
"@harnessio/react-ssca-manager-client": "^0.65.0",
"@harnessio/uicore": "^4.1.2",
"@tanstack/react-query": "4.20.4",

View File

@ -23,7 +23,7 @@ interface SecretFormInputProps<T> {
name: string
scope: Scope
spaceIdFieldName: string
label: React.ReactNode
label?: React.ReactNode
disabled?: boolean
placeholder?: string
formik?: FormikProps<T>

View File

@ -21,4 +21,5 @@
:root {
--har-table-name-column-min-width: 250px;
--input-element-width: 580px;
}

View File

@ -0,0 +1,24 @@
/*
* 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.
*/
.labelContainer {
justify-content: space-between;
align-items: center;
}
.dropdownSelect {
margin-bottom: var(--spacing-xsmall);
}

View File

@ -0,0 +1,20 @@
/*
* 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 dropdownSelect: string
export declare const labelContainer: string

View File

@ -0,0 +1,125 @@
/*
* 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, { useEffect } from 'react'
import { get } from 'lodash-es'
import classNames from 'classnames'
import { useFormikContext } from 'formik'
import { FormGroup } from '@blueprintjs/core'
import { Container, FormInput, Label, Layout } from '@harnessio/uicore'
import { useStrings } from '@ar/frameworks/strings'
import { useAppStore, useParentComponents } from '@ar/hooks'
import css from './MultiTypeSecretInput.module.scss'
export enum SecretValueType {
TEXT = 'TEXT',
ENCRYPTED = 'ENCRYPTED'
}
interface MultiTypeSecretInputProps {
name: string
label: string
typeField: string
secretField: string
secretSpaceIdField: string
helperText?: string
className?: string
labelClassName?: string
placeholder?: string
disabled?: boolean
onChangeType?: (type: SecretValueType) => void
onChangeValue?: (e: React.FormEvent<HTMLElement>, type: SecretValueType) => void
}
export default function MultiTypeSecretInput(props: MultiTypeSecretInputProps) {
const {
name,
className,
labelClassName,
label,
typeField,
onChangeType,
onChangeValue,
placeholder,
disabled,
secretField,
secretSpaceIdField
} = props
const formik = useFormikContext()
const valueType = get(formik.values, typeField)
const { scope } = useAppStore()
const { getString } = useStrings()
const { SecretFormInput } = useParentComponents()
useEffect(() => {
if (!valueType) {
formik.setFieldValue(typeField, SecretValueType.TEXT)
}
}, [])
return (
<FormGroup>
<Layout.Vertical className={className}>
<Layout.Horizontal className={classNames(labelClassName, css.labelContainer)}>
<Label>{label}</Label>
<FormInput.DropDown
className={css.dropdownSelect}
name={typeField}
items={[
{ label: getString('plaintext'), value: SecretValueType.TEXT },
{ label: getString('encrypted'), value: SecretValueType.ENCRYPTED }
]}
onChange={option => {
onChangeType?.(option.value as SecretValueType)
formik.setFieldValue(name, undefined)
formik.setFieldValue(secretField, undefined)
formik.setFieldValue(secretSpaceIdField, undefined)
}}
dropDownProps={{
isLabel: true,
filterable: false,
minWidth: 'unset'
}}
/>
</Layout.Horizontal>
<Container>
{valueType === SecretValueType.TEXT && (
<FormInput.Text
name={name}
placeholder={placeholder}
onChange={e => onChangeValue?.(e, valueType)}
disabled={disabled}
/>
)}
{valueType === SecretValueType.ENCRYPTED && (
<SecretFormInput
name={secretField}
spaceIdFieldName={secretSpaceIdField}
placeholder={placeholder}
scope={scope}
disabled={disabled}
formik={formik}
/>
)}
</Container>
</Layout.Vertical>
</FormGroup>
)
}

View File

@ -79,7 +79,7 @@ export const Tags = (props: TagsComponentProps): JSX.Element => {
}, [hasValue])
return (
<Container>
<Container margin={{ bottom: isTagsOpen ? 'none' : 'small' }}>
<Label className={cx(Classes.LABEL, css.descriptionLabel)} data-tooltip-id={props.dataTooltipId}>
{isOptional ? getString('optionalField', { name: getString('tagsLabel') }) : getString('tagsLabel')}
{props.dataTooltipId ? <HarnessDocTooltip useStandAlone={true} tooltipId={props.dataTooltipId} /> : null}

View File

@ -28,7 +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?):\/\/)?(www.)?[a-z0-9]+(\.[a-z]{2,}){1,3}(#?\/?[a-zA-Z0-9#]+)*\/?(\?[a-zA-Z0-9-_]+=[a-zA-Z0-9-%]+&?)?$/
/((https):\/\/)(www.)?[a-z0-9]+(\.[a-z]{2,}){1,3}(#?\/?[a-zA-Z0-9#]+)*\/?(\?[a-zA-Z0-9-_]+=[a-zA-Z0-9-%]+&?)?$/
export enum PreferenceScope {
USER = 'USER',

View File

@ -15,6 +15,7 @@
*/
import type { IconName } from '@harnessio/icons'
import type { UpstreamRepositoryURLInputSource } from '@ar/pages/upstream-proxy-details/types'
import type { FormikFowardRef, RepositoryPackageType, RepositoryConfigType, PageType, Scanners } from '@ar/common/types'
export interface CreateRepositoryFormProps {
@ -56,6 +57,7 @@ export abstract class RepositoryStep<T, U = unknown> {
protected repositoryIconColor?: string
protected repositoryIconSize?: number
protected supportedScanners?: Scanners[]
protected supportedUpstreamURLSources?: UpstreamRepositoryURLInputSource[]
getPackageType(): string {
return this.packageType
@ -65,6 +67,10 @@ export abstract class RepositoryStep<T, U = unknown> {
return this.supportedScanners ?? []
}
getSupportedUpstreamURLSources(): UpstreamRepositoryURLInputSource[] {
return this.supportedUpstreamURLSources ?? []
}
getDefaultValues(initialValues: T): T {
return { ...this.defaultValues, ...initialValues }
}

View File

@ -32,7 +32,7 @@ import UpstreamProxyActions from '@ar/pages/upstream-proxy-details/components/Up
import UpstreamProxyConfigurationForm from '@ar/pages/upstream-proxy-details/components/Forms/UpstreamProxyConfigurationForm'
import UpstreamProxyCreateFormContent from '@ar/pages/upstream-proxy-details/components/FormContent/UpstreamProxyCreateFormContent'
import {
DockerRepositoryURLInputSource,
UpstreamRepositoryURLInputSource,
UpstreamProxyAuthenticationMode,
UpstreamRegistryRequest
} from '@ar/pages/upstream-proxy-details/types'
@ -51,6 +51,11 @@ export class DockerRepositoryType extends RepositoryStep<VirtualRegistryRequest>
protected repositoryIcon: IconName = 'docker-step'
protected supportedScanners = [Scanners.AQUA_TRIVY]
protected supportsUpstreamProxy = true
protected supportedUpstreamURLSources = [
UpstreamRepositoryURLInputSource.Dockerhub,
UpstreamRepositoryURLInputSource.AwsEcr,
UpstreamRepositoryURLInputSource.Custom
]
protected defaultValues: VirtualRegistryRequest = {
packageType: RepositoryPackageType.DOCKER,
@ -68,7 +73,7 @@ export class DockerRepositoryType extends RepositoryStep<VirtualRegistryRequest>
identifier: '',
config: {
type: RepositoryConfigType.UPSTREAM,
source: DockerRepositoryURLInputSource.Dockerhub,
source: UpstreamRepositoryURLInputSource.Dockerhub,
url: '',
authType: UpstreamProxyAuthenticationMode.ANONYMOUS
},

View File

@ -32,7 +32,7 @@ import UpstreamProxyConfigurationForm from '@ar/pages/upstream-proxy-details/com
import UpstreamProxyCreateFormContent from '@ar/pages/upstream-proxy-details/components/FormContent/UpstreamProxyCreateFormContent'
import UpstreamProxyActions from '@ar/pages/upstream-proxy-details/components/UpstreamProxyActions/UpstreamProxyActions'
import {
DockerRepositoryURLInputSource,
UpstreamRepositoryURLInputSource,
UpstreamProxyAuthenticationMode,
type UpstreamRegistryRequest
} from '@ar/pages/upstream-proxy-details/types'
@ -51,6 +51,10 @@ export class HelmRepositoryType extends RepositoryStep<VirtualRegistryRequest> {
protected repositoryIcon: IconName = 'service-helm'
protected supportedScanners = []
protected supportsUpstreamProxy = true
protected supportedUpstreamURLSources = [
UpstreamRepositoryURLInputSource.AwsEcr,
UpstreamRepositoryURLInputSource.Custom
]
protected defaultValues: VirtualRegistryRequest = {
packageType: RepositoryPackageType.HELM,
@ -67,7 +71,7 @@ export class HelmRepositoryType extends RepositoryStep<VirtualRegistryRequest> {
config: {
type: RepositoryConfigType.UPSTREAM,
authType: UpstreamProxyAuthenticationMode.ANONYMOUS,
source: DockerRepositoryURLInputSource.Custom,
source: UpstreamRepositoryURLInputSource.Custom,
url: ''
},
cleanupPolicy: [],

View File

@ -23,6 +23,6 @@
:global(.bp3-form-group .bp3-input),
:global(.bp3-form-group .bp3-input-group),
:global(.bp3-form-group .bp3-select-popover) {
width: 580px;
width: var(--input-element-width);
}
}

View File

@ -1,66 +0,0 @@
/*
* 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 { FormikContextType, connect } from 'formik'
import { FormInput, Layout } from '@harnessio/uicore'
import { useStrings } from '@ar/frameworks/strings/String'
import { DockerRepositoryURLInputSource, UpstreamRegistryRequest } from '@ar/pages/upstream-proxy-details/types'
interface DockerRepositoryUrlInputProps {
readonly: boolean
}
function DockerRepositoryUrlInput(
props: DockerRepositoryUrlInputProps & { formik: FormikContextType<UpstreamRegistryRequest> }
): JSX.Element {
const { readonly, formik } = props
const { values } = formik
const { getString } = useStrings()
const { config } = values
const { source } = config
return (
<Layout.Vertical spacing="small">
<FormInput.RadioGroup
name="config.source"
radioGroup={{ inline: true }}
disabled={readonly}
label={getString('upstreamProxyDetails.createForm.source.title')}
items={[
{
label: getString('upstreamProxyDetails.createForm.source.dockerHub'),
value: DockerRepositoryURLInputSource.Dockerhub
},
{
label: getString('upstreamProxyDetails.createForm.source.custom'),
value: DockerRepositoryURLInputSource.Custom
}
]}
/>
{source === DockerRepositoryURLInputSource.Custom && (
<FormInput.Text
name="config.url"
label={getString('upstreamProxyDetails.createForm.url')}
placeholder={getString('upstreamProxyDetails.createForm.url')}
disabled={readonly}
/>
)}
</Layout.Vertical>
)
}
export default connect<DockerRepositoryUrlInputProps, UpstreamRegistryRequest>(DockerRepositoryUrlInput)

View File

@ -0,0 +1,29 @@
/*
* 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.
*/
.authContainer {
padding-left: var(--spacing-xlarge) !important;
border-left: 1px solid var(--grey-200);
}
.multiTypeSecretInput {
& .labelContainer {
width: var(--input-element-width) !important;
}
& :global(.bp3-popover) {
width: 180px !important;
}
}

View File

@ -0,0 +1,21 @@
/*
* 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 authContainer: string
export declare const labelContainer: string
export declare const multiTypeSecretInput: string

View File

@ -0,0 +1,124 @@
/*
* 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 { useFormikContext } from 'formik'
import { FontVariation } from '@harnessio/design-system'
import { Container, FormInput, Layout, Text } from '@harnessio/uicore'
import { useStrings } from '@ar/frameworks/strings'
import { useAppStore, useParentComponents } from '@ar/hooks'
import MultiTypeSecretInput from '@ar/components/MultiTypeSecretInput/MultiTypeSecretInput'
import { AuthTypeRadioItems, URLSourceToSupportedAuthTypesMapping } from './constants'
import { UpstreamProxyAuthenticationMode, UpstreamRegistryRequest, UpstreamRepositoryURLInputSource } from '../../types'
import css from './AuthenticationFormInput.module.scss'
interface AuthenticationFormInputProps {
readonly?: boolean
}
export default function AuthenticationFormInput(props: AuthenticationFormInputProps): JSX.Element {
const { readonly } = props
const { scope } = useAppStore()
const { getString } = useStrings()
const { SecretFormInput } = useParentComponents()
const formikProps = useFormikContext<UpstreamRegistryRequest>()
const { values, setFieldValue } = formikProps
const { config, packageType } = values
const { source } = config
const selectedRadioValue = get(values, 'config.authType')
const supportedAuthTypes = URLSourceToSupportedAuthTypesMapping[source as UpstreamRepositoryURLInputSource] || []
const radioItems = supportedAuthTypes.map(each => AuthTypeRadioItems[each])
return (
<Layout.Vertical spacing="small">
<FormInput.RadioGroup
key={`${source}-${packageType}`}
name="config.authType"
onChange={e => {
const selectedValue = e.currentTarget.value as UpstreamProxyAuthenticationMode
if (selectedValue === UpstreamProxyAuthenticationMode.ANONYMOUS) {
setFieldValue('config.auth', null)
} else {
setFieldValue('config.auth', {})
}
}}
radioGroup={{ inline: true }}
label={getString('upstreamProxyDetails.createForm.authentication.title')}
items={radioItems.map(each => ({
...each,
label: (
<Layout.Vertical>
<Text font={{ variation: FontVariation.SMALL }}>{getString(each.label)}</Text>
{each.subLabel && <Text font={{ variation: FontVariation.SMALL }}>{getString(each.subLabel)}</Text>}
</Layout.Vertical>
)
}))}
disabled={readonly}
/>
{selectedRadioValue === UpstreamProxyAuthenticationMode.USER_NAME_AND_PASSWORD && (
<Container className={css.authContainer}>
<Layout.Vertical>
<FormInput.Text
name="config.auth.userName"
label={getString('upstreamProxyDetails.createForm.authentication.username')}
placeholder={getString('upstreamProxyDetails.createForm.authentication.username')}
disabled={readonly}
/>
<SecretFormInput
name="config.auth.secretIdentifier"
spaceIdFieldName="config.auth.secretSpaceId"
label={getString('upstreamProxyDetails.createForm.authentication.password')}
placeholder={getString('upstreamProxyDetails.createForm.authentication.password')}
scope={scope}
disabled={readonly}
formik={formikProps}
/>
</Layout.Vertical>
</Container>
)}
{selectedRadioValue === UpstreamProxyAuthenticationMode.ACCESS_KEY_AND_SECRET_KEY && (
<Container className={css.authContainer}>
<Layout.Vertical>
<MultiTypeSecretInput
className={css.multiTypeSecretInput}
labelClassName={css.labelContainer}
name="config.auth.accessKey"
secretField="config.auth.accessKeySecretIdentifier"
secretSpaceIdField="config.auth.accessKeySecretSpaceId"
typeField="config.auth.accessKeyType"
label={getString('upstreamProxyDetails.createForm.authentication.accessKey')}
placeholder={getString('upstreamProxyDetails.createForm.authentication.accessKey')}
disabled={readonly}
/>
<SecretFormInput
name="config.auth.secretKeyIdentifier"
spaceIdFieldName="config.auth.secretKeySpaceId"
label={getString('upstreamProxyDetails.createForm.authentication.secretKey')}
placeholder={getString('upstreamProxyDetails.createForm.authentication.secretKey')}
scope={scope}
disabled={readonly}
formik={formikProps}
/>
</Layout.Vertical>
</Container>
)}
</Layout.Vertical>
)
}

View File

@ -0,0 +1,58 @@
/*
* 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 { StringsMap } from '@ar/strings/types'
import { UpstreamProxyAuthenticationMode, UpstreamRepositoryURLInputSource } from '../../types'
interface RadioGroupItem {
label: keyof StringsMap
subLabel?: keyof StringsMap
value: UpstreamProxyAuthenticationMode
}
export const AuthTypeRadioItems: Record<UpstreamProxyAuthenticationMode, RadioGroupItem> = {
[UpstreamProxyAuthenticationMode.USER_NAME_AND_PASSWORD]: {
label: 'upstreamProxyDetails.createForm.authentication.userNameAndPassword',
value: UpstreamProxyAuthenticationMode.USER_NAME_AND_PASSWORD
},
[UpstreamProxyAuthenticationMode.ANONYMOUS]: {
label: 'upstreamProxyDetails.createForm.authentication.anonymous',
subLabel: 'upstreamProxyDetails.createForm.authentication.anonymousSubLabel',
value: UpstreamProxyAuthenticationMode.ANONYMOUS
},
[UpstreamProxyAuthenticationMode.ACCESS_KEY_AND_SECRET_KEY]: {
label: 'upstreamProxyDetails.createForm.authentication.accessKeyAndSecretKey',
value: UpstreamProxyAuthenticationMode.ACCESS_KEY_AND_SECRET_KEY
}
}
export const URLSourceToSupportedAuthTypesMapping: Record<
UpstreamRepositoryURLInputSource,
UpstreamProxyAuthenticationMode[]
> = {
[UpstreamRepositoryURLInputSource.Dockerhub]: [
UpstreamProxyAuthenticationMode.USER_NAME_AND_PASSWORD,
UpstreamProxyAuthenticationMode.ANONYMOUS
],
[UpstreamRepositoryURLInputSource.AwsEcr]: [
UpstreamProxyAuthenticationMode.ACCESS_KEY_AND_SECRET_KEY,
UpstreamProxyAuthenticationMode.ANONYMOUS
],
[UpstreamRepositoryURLInputSource.Custom]: [
UpstreamProxyAuthenticationMode.USER_NAME_AND_PASSWORD,
UpstreamProxyAuthenticationMode.ANONYMOUS
]
}

View File

@ -15,94 +15,22 @@
*/
import React from 'react'
import { get } from 'lodash-es'
import type { FormikProps } from 'formik'
import { FontVariation } from '@harnessio/design-system'
import { Container, FormInput, Layout, Text } from '@harnessio/uicore'
import { Layout } from '@harnessio/uicore'
import { useStrings } from '@ar/frameworks/strings'
import { useAppStore, useParentComponents } from '@ar/hooks'
import { UpstreamProxyAuthenticationMode, type UpstreamRegistryRequest } from '../../types'
import css from './FormContent.module.scss'
import RepositoryUrlInput from '../RepositoryUrlInput/RepositoryUrlInput'
import AuthenticationFormInput from '../AuthenticationFormInput/AuthenticationFormInput'
interface UpstreamProxyAuthenticationFormContentProps {
formikProps: FormikProps<UpstreamRegistryRequest>
readonly: boolean
}
export default function UpstreamProxyAuthenticationFormContent({
formikProps,
readonly
}: UpstreamProxyAuthenticationFormContentProps): JSX.Element {
const { getString } = useStrings()
const { scope } = useAppStore()
const { SecretFormInput } = useParentComponents()
const selectedRadioValue = get(formikProps.values, 'config.authType')
return (
<Layout.Vertical spacing="small">
<FormInput.RadioGroup
name="config.authType"
onChange={e => {
const selectedValue = e.currentTarget.value as UpstreamProxyAuthenticationMode
if (selectedValue === UpstreamProxyAuthenticationMode.ANONYMOUS) {
formikProps.setFieldValue('config.auth', null)
} else {
formikProps.setFieldValue('config.auth', {})
}
}}
radioGroup={{ inline: true }}
label={getString('upstreamProxyDetails.createForm.authentication.title')}
items={[
{
label: getString('upstreamProxyDetails.createForm.authentication.userNameAndPassword'),
value: UpstreamProxyAuthenticationMode.USER_NAME_AND_PASSWORD
},
{
label: (
<Layout.Vertical>
<Text font={{ variation: FontVariation.SMALL }}>
{getString('upstreamProxyDetails.createForm.authentication.anonymous')}
</Text>
<Text font={{ variation: FontVariation.SMALL }}>
{getString('upstreamProxyDetails.createForm.authentication.anonymousSubLabel')}
</Text>
</Layout.Vertical>
),
value: UpstreamProxyAuthenticationMode.ANONYMOUS
}
]}
disabled={readonly}
/>
{selectedRadioValue === UpstreamProxyAuthenticationMode.USER_NAME_AND_PASSWORD && (
<Container className={css.authContainer}>
<Layout.Vertical>
<FormInput.Text
name="config.auth.userName"
label={getString('upstreamProxyDetails.createForm.authentication.username')}
placeholder={getString('upstreamProxyDetails.createForm.authentication.username')}
disabled={readonly}
/>
{/* <FormInput.Text
name="config.auth.password"
label={getString('upstreamProxyDetails.createForm.authentication.password')}
placeholder={getString('upstreamProxyDetails.createForm.authentication.password')}
disabled={readonly}
inputGroup={{
type: 'password'
}}
/> */}
<SecretFormInput
name="config.auth.secretIdentifier"
spaceIdFieldName="config.auth.secretSpaceId"
label={getString('upstreamProxyDetails.createForm.authentication.password')}
placeholder={getString('upstreamProxyDetails.createForm.authentication.password')}
scope={scope}
disabled={readonly}
formik={formikProps}
/>
</Layout.Vertical>
</Container>
)}
<RepositoryUrlInput readonly={readonly} />
<AuthenticationFormInput readonly={readonly} />
</Layout.Vertical>
)
}

View File

@ -72,7 +72,7 @@ export default function UpstreamProxyConfigurationFormContent(
<Card className={classNames(css.cardContainer, css.marginTopLarge)}>
<Layout.Vertical>
<UpstreamProxyDetailsFormContent isEdit formikProps={formikProps} readonly={readonly} />
<UpstreamProxyAuthenticationFormContent formikProps={formikProps} readonly={readonly} />
<UpstreamProxyAuthenticationFormContent readonly={readonly} />
</Layout.Vertical>
</Card>
{parent === Parent.Enterprise && (

View File

@ -41,7 +41,7 @@ function UpstreamProxyCreateFormContent(
{getString('upstreamProxyDetails.form.title')}
</Text>
<UpstreamProxyDetailsFormContent isEdit={isEdit} formikProps={formik} readonly={readonly} />
<UpstreamProxyAuthenticationFormContent formikProps={formik} readonly={readonly} />
<UpstreamProxyAuthenticationFormContent readonly={readonly} />
</Layout.Vertical>
</FormikForm>
)

View File

@ -15,11 +15,13 @@
*/
import React from 'react'
import { isEmpty } from 'lodash-es'
import type { FormikProps } from 'formik'
import { FormInput, Layout } from '@harnessio/uicore'
import { useStrings } from '@ar/frameworks/strings/String'
import RepositoryUrlInput from '../RepositoryUrlInput/RepositoryUrlInput'
import { Description, Tags } from '@ar/components/NameDescriptionTags'
import type { UpstreamRegistryRequest } from '../../types'
interface UpstreamProxyDetailsFormContentProps {
@ -31,7 +33,7 @@ interface UpstreamProxyDetailsFormContentProps {
export default function UpstreamProxyDetailsFormContent(props: UpstreamProxyDetailsFormContentProps): JSX.Element {
const { readonly, isEdit, formikProps } = props
const { values } = formikProps
const { packageType } = values
const { description, labels } = values
const { getString } = useStrings()
return (
<Layout.Vertical>
@ -44,7 +46,8 @@ export default function UpstreamProxyDetailsFormContent(props: UpstreamProxyDeta
autoFocus: true
}}
/>
<RepositoryUrlInput packageType={packageType} readonly={readonly} />
<Description hasValue={!!description} disabled={readonly} />
<Tags name="labels" hasValue={!isEmpty(labels)} disabled={readonly} />
</Layout.Vertical>
)
}

View File

@ -18,7 +18,7 @@
:global(.bp3-form-group .bp3-input),
:global(.bp3-form-group .bp3-input-group),
:global(.bp3-form-group .bp3-select-popover) {
width: 580px;
width: var(--input-element-width);
}
}

View File

@ -17,9 +17,8 @@
import React, { forwardRef, useContext } from 'react'
import * as Yup from 'yup'
import { Formik, FormikForm, getErrorInfoFromErrorObject, useToaster } from '@harnessio/uicore'
import { Anonymous, UserPassword, useModifyRegistryMutation } from '@harnessio/react-har-service-client'
import { useModifyRegistryMutation } from '@harnessio/react-har-service-client'
import { URL_REGEX } from '@ar/constants'
import { useAppStore, useGetSpaceRef } from '@ar/hooks'
import { useStrings } from '@ar/frameworks/strings'
import { queryClient } from '@ar/utils/queryClient'
@ -35,13 +34,12 @@ import {
} from '@ar/components/CleanupPolicyList/utils'
import UpstreamProxyConfigurationFormContent from '../FormContent/UpstreamProxyConfigurationFormContent'
import type { UpstreamRegistry, UpstreamRegistryRequest } from '../../types'
import {
DockerRepositoryURLInputSource,
UpstreamProxyAuthenticationMode,
type UpstreamRegistry,
type UpstreamRegistryRequest
} from '../../types'
import { getFormattedFormDataForAuthType, getFormattedInitialValuesForAuthType } from './utils'
getFormattedFormDataForAuthType,
getFormattedInitialValuesForAuthType,
getValidationSchemaForUpstreamForm
} from './utils'
import css from './Forms.module.scss'
@ -115,45 +113,7 @@ function UpstreamProxyConfigurationForm(
}}
formName="upstream-repository-form"
initialValues={getInitialValues(data as UpstreamRegistry)}
validationSchema={Yup.object().shape({
config: Yup.object().shape({
authType: Yup.string()
.required()
.oneOf([UpstreamProxyAuthenticationMode.ANONYMOUS, UpstreamProxyAuthenticationMode.USER_NAME_AND_PASSWORD]),
auth: Yup.object()
.when(['authType'], {
is: (authType: UpstreamProxyAuthenticationMode) =>
authType === UpstreamProxyAuthenticationMode.USER_NAME_AND_PASSWORD,
then: (schema: Yup.ObjectSchema<UserPassword | Anonymous>) =>
schema.shape({
userName: Yup.string().trim().required(getString('validationMessages.userNameRequired')),
secretIdentifier: Yup.string().trim().required(getString('validationMessages.passwordRequired'))
}),
otherwise: Yup.object().optional().nullable()
})
.nullable(),
url: Yup.string().when(['source'], {
is: (source: DockerRepositoryURLInputSource) => source === DockerRepositoryURLInputSource.Custom,
then: (schema: Yup.StringSchema) =>
schema
.trim()
.required(getString('validationMessages.urlRequired'))
.matches(URL_REGEX, getString('validationMessages.urlPattern')),
otherwise: (schema: Yup.StringSchema) => schema.trim().notRequired()
})
}),
cleanupPolicy: Yup.array()
.of(
Yup.object().shape({
name: Yup.string().trim().required(getString('validationMessages.cleanupPolicy.nameRequired')),
expireDays: Yup.number()
.required(getString('validationMessages.cleanupPolicy.expireDaysRequired'))
.positive(getString('validationMessages.cleanupPolicy.positiveExpireDays'))
})
)
.optional()
.nullable()
})}>
validationSchema={Yup.object().shape(getValidationSchemaForUpstreamForm(getString))}>
{formik => {
setFormikRef(formikRef, formik)
return (

View File

@ -27,18 +27,16 @@ import {
getErrorInfoFromErrorObject,
useToaster
} from '@harnessio/uicore'
import { Anonymous, UserPassword, useCreateRegistryMutation } from '@harnessio/react-har-service-client'
import { useCreateRegistryMutation } from '@harnessio/react-har-service-client'
import { useAppStore, useGetSpaceRef } from '@ar/hooks'
import { useStrings } from '@ar/frameworks/strings'
import { decodeRef } from '@ar/hooks/useGetSpaceRef'
import { setFormikRef } from '@ar/common/utils'
import { REPO_KEY_REGEX, URL_REGEX } from '@ar/constants'
import { REPO_KEY_REGEX } from '@ar/constants'
import { Separator } from '@ar/components/Separator/Separator'
import { RepositoryConfigType, type FormikFowardRef } from '@ar/common/types'
import {
DockerRepositoryURLInputSource,
UpstreamProxyAuthenticationMode,
UpstreamProxyPackageType,
UpstreamRegistry,
UpstreamRegistryRequest
@ -50,7 +48,7 @@ import repositoryFactory from '@ar/frameworks/RepositoryStep/RepositoryFactory'
import { getFormattedFormDataForCleanupPolicy } from '@ar/components/CleanupPolicyList/utils'
import type { RepositoryAbstractFactory } from '@ar/frameworks/RepositoryStep/RepositoryAbstractFactory'
import { getFormattedFormDataForAuthType } from './utils'
import { getFormattedFormDataForAuthType, getValidationSchemaForUpstreamForm } from './utils'
import css from './Forms.module.scss'
interface FormContentProps {
@ -190,31 +188,7 @@ function UpstreamProxyCreateForm(props: UpstreamProxyCreateFormProps, formikRef:
identifier: Yup.string()
.required(getString('validationMessages.nameRequired'))
.matches(REPO_KEY_REGEX, getString('validationMessages.repokeyRegExMessage')),
config: Yup.object().shape({
authType: Yup.string()
.required()
.oneOf([UpstreamProxyAuthenticationMode.ANONYMOUS, UpstreamProxyAuthenticationMode.USER_NAME_AND_PASSWORD]),
auth: Yup.object()
.when(['authType'], {
is: (authType: UpstreamProxyAuthenticationMode) =>
authType === UpstreamProxyAuthenticationMode.USER_NAME_AND_PASSWORD,
then: (schema: Yup.ObjectSchema<UserPassword | Anonymous>) =>
schema.shape({
userName: Yup.string().trim().required(getString('validationMessages.userNameRequired')),
secretIdentifier: Yup.string().trim().required(getString('validationMessages.passwordRequired'))
})
})
.nullable(),
url: Yup.string().when(['source'], {
is: (source: DockerRepositoryURLInputSource) => source === DockerRepositoryURLInputSource.Custom,
then: (schema: Yup.StringSchema) =>
schema
.trim()
.required(getString('validationMessages.urlRequired'))
.matches(URL_REGEX, getString('validationMessages.urlPattern')),
otherwise: (schema: Yup.StringSchema) => schema.trim().notRequired()
})
})
...getValidationSchemaForUpstreamForm(getString)
})}>
{(formik: FormikProps<UpstreamRegistryRequest>) => {
setFormikRef(formikRef, formik)

View File

@ -15,14 +15,18 @@
*/
import produce from 'immer'
import { compact, defaultTo, get, set } from 'lodash-es'
import type { UserPassword } from '@harnessio/react-har-service-client'
import * as Yup from 'yup'
import { compact, get, isEmpty, set } from 'lodash-es'
import type { AccessKeySecretKey, Anonymous, UserPassword } from '@harnessio/react-har-service-client'
import { Parent } from '@ar/common/types'
import { URL_REGEX } from '@ar/constants'
import type { Scope } from '@ar/MFEAppTypes'
import type { StringKeys } from '@ar/frameworks/strings'
import { SecretValueType } from '@ar/components/MultiTypeSecretInput/MultiTypeSecretInput'
import {
DockerRepositoryURLInputSource,
UpstreamRepositoryURLInputSource,
UpstreamProxyAuthenticationMode,
type UpstreamRegistryRequest
} from '../../types'
@ -46,6 +50,41 @@ export function getReferenceStringFromSecretSpacePath(identifier: string, secret
return identifier
}
function convertSecretInputToFormFields(
formData: UpstreamRegistryRequest,
secretField: string,
secretSpacePathField: string,
scope?: Scope
) {
const password = get(formData, secretField)
set(formData, secretSpacePathField, getSecretSpacePath(get(password, 'referenceString', ''), scope))
set(formData, secretField, get(password, 'identifier'))
}
function convertMultiTypeSecretInputToFormFields(
formData: UpstreamRegistryRequest,
typeField: string,
textField: string,
secretField: string,
secretSpacePathField: string,
scope?: Scope
) {
const accessKeyType = get(formData, typeField)
if (accessKeyType === SecretValueType.TEXT) {
const value = get(formData, textField, '')
set(formData, textField, value)
set(formData, typeField, undefined)
set(formData, secretField, undefined)
set(formData, secretSpacePathField, undefined)
} else {
const accessKeySecret = get(formData, secretField)
set(formData, secretSpacePathField, getSecretSpacePath(get(accessKeySecret, 'referenceString', ''), scope))
set(formData, secretField, get(accessKeySecret, 'identifier'))
set(formData, typeField, undefined)
set(formData, textField, undefined)
}
}
export function getFormattedFormDataForAuthType(
values: UpstreamRegistryRequest,
parent?: Parent,
@ -55,13 +94,30 @@ export function getFormattedFormDataForAuthType(
if (draft.config.authType === UpstreamProxyAuthenticationMode.USER_NAME_AND_PASSWORD) {
set(draft, 'config.auth.authType', draft.config.authType)
if (parent === Parent.Enterprise) {
const auth = draft.config.auth as UserPassword
const password = auth?.secretIdentifier
set(draft, 'config.auth.secretSpacePath', getSecretSpacePath(get(password, 'referenceString', ''), scope))
set(draft, 'config.auth.secretIdentifier', get(password, 'identifier'))
convertSecretInputToFormFields(draft, 'config.auth.secretIdentifier', 'config.auth.secretSpacePath', scope)
}
} else if (draft.config.authType === UpstreamProxyAuthenticationMode.ACCESS_KEY_AND_SECRET_KEY) {
set(draft, 'config.auth.authType', draft.config.authType)
if (parent === Parent.Enterprise) {
convertSecretInputToFormFields(
draft,
'config.auth.secretKeyIdentifier',
'config.auth.secretKeySpacePath',
scope
)
convertMultiTypeSecretInputToFormFields(
draft,
'config.auth.accessKeyType',
'config.auth.accessKey',
'config.auth.accessKeySecretIdentifier',
'config.auth.accessKeySecretSpacePath',
scope
)
}
} else if (draft.config.authType === UpstreamProxyAuthenticationMode.ANONYMOUS) {
set(draft, 'config.auth', null)
}
if (draft.config.source !== DockerRepositoryURLInputSource.Custom) {
if (draft.config.source === UpstreamRepositoryURLInputSource.Dockerhub) {
set(draft, 'config.url', '')
}
})
@ -79,15 +135,137 @@ function getSecretScopeDetailsByIdentifier(identifier: string, secretSpacePath:
}
}
function convertFormFieldsToSecreteInput(
formData: UpstreamRegistryRequest,
secretField: string,
secretSpacePathField: string
) {
const secretIdentifier = get(formData, secretField, '')
const secretSpacePath = get(formData, secretSpacePathField, '')
set(formData, secretField, getSecretScopeDetailsByIdentifier(secretIdentifier, secretSpacePath))
}
function convertFormFieldsToMultiTypeSecretInput(
formData: UpstreamRegistryRequest,
typeField: string,
formField: string,
secretIdentifierField: string,
secretSpacePathField: string
) {
const value = get(formData, formField)
if (value) {
set(formData, formField, value)
set(formData, typeField, SecretValueType.TEXT)
set(formData, secretIdentifierField, undefined)
set(formData, secretSpacePathField, undefined)
} else {
const secretIdentifierValue = get(formData, secretIdentifierField, '')
const secretSpacePathValue = get(formData, secretSpacePathField, '')
set(formData, typeField, SecretValueType.ENCRYPTED)
set(formData, secretIdentifierField, getSecretScopeDetailsByIdentifier(secretIdentifierValue, secretSpacePathValue))
set(formData, formField, undefined)
}
}
export function getFormattedInitialValuesForAuthType(values: UpstreamRegistryRequest, parent?: Parent) {
return produce(values, (draft: UpstreamRegistryRequest) => {
if (draft.config.authType === UpstreamProxyAuthenticationMode.USER_NAME_AND_PASSWORD) {
if (parent === Parent.Enterprise) {
const auth = draft.config.auth as UserPassword
const secretIdentifier = defaultTo(auth?.secretIdentifier, '')
const secretSpacePath = defaultTo(auth?.secretSpacePath, '')
set(draft, 'config.auth.secretIdentifier', getSecretScopeDetailsByIdentifier(secretIdentifier, secretSpacePath))
convertFormFieldsToSecreteInput(draft, 'config.auth.secretIdentifier', 'config.auth.secretSpacePath')
}
}
if (draft.config.authType === UpstreamProxyAuthenticationMode.ACCESS_KEY_AND_SECRET_KEY) {
if (parent === Parent.Enterprise) {
convertFormFieldsToSecreteInput(draft, 'config.auth.secretKeyIdentifier', 'config.auth.secretKeySpacePath')
convertFormFieldsToMultiTypeSecretInput(
draft,
'config.auth.accessKeyType',
'config.auth.accessKey',
'config.auth.accessKeySecretIdentifier',
'config.auth.accessKeySecretSpacePath'
)
} else {
const accessKeyType = isEmpty(get(draft, 'config.auth.accessKey'))
? SecretValueType.ENCRYPTED
: SecretValueType.TEXT
set(draft, 'config.auth.accessKeyType', accessKeyType)
}
}
})
}
export function getValidationSchemaForUpstreamForm(getString: (key: StringKeys, vars?: Record<string, any>) => string) {
return {
config: Yup.object().shape({
authType: Yup.string()
.required()
.oneOf([
UpstreamProxyAuthenticationMode.ANONYMOUS,
UpstreamProxyAuthenticationMode.USER_NAME_AND_PASSWORD,
UpstreamProxyAuthenticationMode.ACCESS_KEY_AND_SECRET_KEY
]),
auth: Yup.object()
.when(['authType'], {
is: (authType: UpstreamProxyAuthenticationMode) =>
authType === UpstreamProxyAuthenticationMode.USER_NAME_AND_PASSWORD,
then: (schema: Yup.ObjectSchema<UserPassword | Anonymous>) =>
schema.shape({
userName: Yup.string().trim().required(getString('validationMessages.userNameRequired')),
secretIdentifier: Yup.string().trim().required(getString('validationMessages.passwordRequired'))
}),
otherwise: Yup.object().optional().nullable()
})
.when(['authType'], {
is: (authType: UpstreamProxyAuthenticationMode) =>
authType === UpstreamProxyAuthenticationMode.ACCESS_KEY_AND_SECRET_KEY,
then: (schema: Yup.ObjectSchema<AccessKeySecretKey | Anonymous>) =>
schema.shape({
accessKey: Yup.string()
.trim()
.test('access-key-validation', getString('validationMessages.accessKeyRequired'), function (value) {
if (this.parent.accessKeyType === SecretValueType.TEXT) {
return !isEmpty(value)
}
return true
}),
accessKeySecretIdentifier: Yup.string()
.trim()
.test(
'access-key-secret-validation',
getString('validationMessages.accessKeyRequired'),
function (value) {
if (this.parent.accessKeyType === SecretValueType.ENCRYPTED) {
return !isEmpty(value)
}
return true
}
),
secretKeyIdentifier: Yup.string().trim().required(getString('validationMessages.secretKeyRequired'))
}),
otherwise: Yup.object().optional().nullable()
})
.nullable(),
url: Yup.string().when(['source'], {
is: (source: UpstreamRepositoryURLInputSource) =>
[UpstreamRepositoryURLInputSource.Custom, UpstreamRepositoryURLInputSource.AwsEcr].includes(source),
then: (schema: Yup.StringSchema) =>
schema
.trim()
.required(getString('validationMessages.urlRequired'))
.matches(URL_REGEX, getString('validationMessages.urlPattern')),
otherwise: (schema: Yup.StringSchema) => schema.trim().notRequired()
})
}),
cleanupPolicy: Yup.array()
.of(
Yup.object().shape({
name: Yup.string().trim().required(getString('validationMessages.cleanupPolicy.nameRequired')),
expireDays: Yup.number()
.required(getString('validationMessages.cleanupPolicy.expireDaysRequired'))
.positive(getString('validationMessages.cleanupPolicy.positiveExpireDays'))
})
)
.optional()
.nullable()
}
}

View File

@ -15,30 +15,57 @@
*/
import React from 'react'
import { FormInput } from '@harnessio/uicore'
import type { PackageType } from '@harnessio/react-har-service-client'
import { useFormikContext } from 'formik'
import { FormInput, Layout } from '@harnessio/uicore'
import { useStrings } from '@ar/frameworks/strings/String'
import { UpstreamProxyPackageType } from '@ar/pages/upstream-proxy-details/types'
import DockerRepositoryUrlInput from '@ar/pages/upstream-proxy-details/DockerRepository/DockerRepositoryUrlInput/DockerRepositoryUrlInput'
import repositoryFactory from '@ar/frameworks/RepositoryStep/RepositoryFactory'
import { UpstreamURLSourceConfig } from './constants'
import { UpstreamProxyAuthenticationMode, UpstreamRegistryRequest, UpstreamRepositoryURLInputSource } from '../../types'
interface RepositoryUrlInputProps {
readonly: boolean
packageType: PackageType
}
export default function RepositoryUrlInput(props: RepositoryUrlInputProps): JSX.Element {
const { getString } = useStrings()
const { readonly, packageType } = props
if (packageType === UpstreamProxyPackageType.DOCKER) {
return <DockerRepositoryUrlInput readonly={readonly} />
}
const { readonly } = props
const { values, setFieldValue } = useFormikContext<UpstreamRegistryRequest>()
const { packageType, config } = values
const { source } = config
const repositoryType = repositoryFactory.getRepositoryType(packageType)
const supportedURLSources = repositoryType?.getSupportedUpstreamURLSources() || []
const radioItems = supportedURLSources.map(each => UpstreamURLSourceConfig[each])
return (
<FormInput.Text
name="config.url"
label={getString('upstreamProxyDetails.createForm.url')}
placeholder={getString('upstreamProxyDetails.createForm.url')}
disabled={readonly}
/>
<Layout.Vertical spacing="small">
{radioItems.length ? (
<FormInput.RadioGroup
key={packageType}
name="config.source"
radioGroup={{ inline: true }}
disabled={readonly}
label={getString('upstreamProxyDetails.createForm.source.title')}
items={radioItems.map(each => ({ ...each, label: getString(each.label) }))}
onChange={e => {
const selectedValue = e.currentTarget.value as UpstreamRepositoryURLInputSource
if (source !== selectedValue) {
setFieldValue('config.url', '')
setFieldValue('config.authType', UpstreamProxyAuthenticationMode.ANONYMOUS)
}
}}
/>
) : null}
{[UpstreamRepositoryURLInputSource.Custom, UpstreamRepositoryURLInputSource.AwsEcr].includes(
source as UpstreamRepositoryURLInputSource
) && (
<FormInput.Text
name="config.url"
label={getString('upstreamProxyDetails.createForm.url')}
placeholder={getString('upstreamProxyDetails.createForm.url')}
disabled={readonly}
/>
)}
</Layout.Vertical>
)
}

View File

@ -0,0 +1,37 @@
/*
* 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 { StringsMap } from '@ar/frameworks/strings'
import { UpstreamRepositoryURLInputSource } from '../../types'
interface RadioGroupItem {
label: keyof StringsMap
value: UpstreamRepositoryURLInputSource
}
export const UpstreamURLSourceConfig: Record<UpstreamRepositoryURLInputSource, RadioGroupItem> = {
[UpstreamRepositoryURLInputSource.Dockerhub]: {
label: 'upstreamProxyDetails.createForm.source.dockerHub',
value: UpstreamRepositoryURLInputSource.Dockerhub
},
[UpstreamRepositoryURLInputSource.AwsEcr]: {
label: 'upstreamProxyDetails.createForm.source.ecr',
value: UpstreamRepositoryURLInputSource.AwsEcr
},
[UpstreamRepositoryURLInputSource.Custom]: {
label: 'upstreamProxyDetails.createForm.source.custom',
value: UpstreamRepositoryURLInputSource.Custom
}
}

View File

@ -10,14 +10,18 @@ createForm:
source:
title: Source
dockerHub: Docker Hub
ecr: AWS ECR
custom: Custom
authentication:
title: Authentication
userNameAndPassword: Username and Password
accessKeyAndSecretKey: Access Key and Secret Key
anonymous: Anonymous
anonymousSubLabel: (No credentials required)
username: Username
password: Password
secretKey: Secret Key
accessKey: Access Key
create: Create Upstream Proxy
editForm:
title: Update Upstream Proxy

View File

@ -24,13 +24,15 @@ export enum UpstreamProxyPackageType {
MAVEN = 'MAVEN'
}
export enum DockerRepositoryURLInputSource {
export enum UpstreamRepositoryURLInputSource {
Dockerhub = 'Dockerhub',
AwsEcr = 'AwsEcr',
Custom = 'Custom'
}
export enum UpstreamProxyAuthenticationMode {
USER_NAME_AND_PASSWORD = 'UserPassword',
ACCESS_KEY_AND_SECRET_KEY = 'AccessKeySecretKey',
ANONYMOUS = 'Anonymous'
}

View File

@ -37,6 +37,8 @@ noDescription: No Description
lastUpdated: Last Updated
createdAt: Created At
modifiedAt: Modified At
plaintext: Plain Text
encrypted: Encrypted
tags:
latest: LATEST
latestVersion: LATEST VERSION
@ -93,3 +95,5 @@ validationMessages:
urlPattern: Remote registry URL must be valid
userNameRequired: Username is required
passwordRequired: Password is required
accessKeyRequired: Access key is required
secretKeyRequired: Secret key is required

View File

@ -124,9 +124,12 @@ export interface StringsMap {
'upstreamProxyDetails.actions.delete.contentText': string
'upstreamProxyDetails.actions.delete.repositoryDeleted': string
'upstreamProxyDetails.actions.delete.title': string
'upstreamProxyDetails.createForm.authentication.accessKey': string
'upstreamProxyDetails.createForm.authentication.accessKeyAndSecretKey': string
'upstreamProxyDetails.createForm.authentication.anonymous': string
'upstreamProxyDetails.createForm.authentication.anonymousSubLabel': string
'upstreamProxyDetails.createForm.authentication.password': string
'upstreamProxyDetails.createForm.authentication.secretKey': string
'upstreamProxyDetails.createForm.authentication.title': string
'upstreamProxyDetails.createForm.authentication.userNameAndPassword': string
'upstreamProxyDetails.createForm.authentication.username': string
@ -135,6 +138,7 @@ export interface StringsMap {
'upstreamProxyDetails.createForm.packageType': string
'upstreamProxyDetails.createForm.source.custom': string
'upstreamProxyDetails.createForm.source.dockerHub': string
'upstreamProxyDetails.createForm.source.ecr': string
'upstreamProxyDetails.createForm.source.title': string
'upstreamProxyDetails.createForm.title': string
'upstreamProxyDetails.createForm.url': string
@ -257,6 +261,7 @@ export interface StringsMap {
descriptionPlaceholder: string
discard: string
download: string
encrypted: string
enterPlaceholder: string
failedToLoadData: string
harLabel: string
@ -277,6 +282,7 @@ export interface StringsMap {
'packageTypes.dockerPackage': string
'packageTypes.genericPackage': string
'packageTypes.helmPackage': string
plaintext: string
plusNewName: string
prod: string
prodCount: string
@ -299,12 +305,14 @@ export interface StringsMap {
'tags.latestVersion': string
tagsLabel: string
timeLabel: string
'validationMessages.accessKeyRequired': string
'validationMessages.cleanupPolicy.expireDaysRequired': string
'validationMessages.cleanupPolicy.nameRequired': string
'validationMessages.cleanupPolicy.positiveExpireDays': string
'validationMessages.nameRequired': string
'validationMessages.passwordRequired': string
'validationMessages.repokeyRegExMessage': string
'validationMessages.secretKeyRequired': string
'validationMessages.urlPattern': string
'validationMessages.urlRequired': string
'validationMessages.userNameRequired': string

View File

@ -14,6 +14,20 @@
* limitations under the License.
*/
.createNewBtn {
margin-top: 5px !important;
.container {
display: flex;
justify-content: flex-start;
align-items: center;
.createNewBtn {
margin-top: 5px !important;
}
&.containerWithoutLabel {
align-items: flex-start !important;
.createNewBtn {
margin-top: 0 !important;
}
}
}

View File

@ -16,4 +16,6 @@
/* eslint-disable */
// This is an auto-generated file
export declare const container: string
export declare const containerWithoutLabel: string
export declare const createNewBtn: string

View File

@ -16,6 +16,7 @@
import React from 'react'
import { get } from 'lodash-es'
import classNames from 'classnames'
import { useFormikContext } from 'formik'
import { useMemo, useState } from 'react'
import { ButtonVariation, FormInput, Layout, SelectOption } from '@harnessio/uicore'
@ -73,7 +74,12 @@ export default function SecretFormInput(props: SecretFormInputProps) {
const selectedValue = formikValue ? { label: formikValue, value: formikValue } : null
return (
<Layout.Horizontal spacing="small" flex={{ justifyContent: 'flex-start', alignItems: 'center' }}>
<Layout.Horizontal
className={classNames(css.container, {
[css.containerWithoutLabel]: !label
})}
spacing="small"
flex={{ justifyContent: 'flex-start', alignItems: 'center' }}>
<FormInput.Select
name={name}
label={label}

View File

@ -1945,10 +1945,10 @@
yargs "^17.6.2"
zod "^3.19.1"
"@harnessio/react-har-service-client@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@harnessio/react-har-service-client/-/react-har-service-client-0.3.0.tgz#b434034093c6619effe42720c7c078d18a9ecc50"
integrity sha512-+pFkxlUUM9CEkB60kx3SjppCdHdycfJhRN6t4p1R0M/n93dQLrdI0uHkoDZEeWb0o1Q0oQ21pkwTYktQr24E3g==
"@harnessio/react-har-service-client@^0.4.0":
version "0.4.0"
resolved "https://registry.yarnpkg.com/@harnessio/react-har-service-client/-/react-har-service-client-0.4.0.tgz#ad0deb70502f8ff51c7199d72dd67aa7a13640b4"
integrity sha512-ndVz0Ig0FHHwgDy1oZVQ7I2epFgM0dnYLLAUHOLMzaqvcV6pN8RCuawtygcFi9K2DKMfFQieWTuI9UjVoB8M+w==
dependencies:
"@harnessio/oats-cli" "^3.0.0"