diff --git a/src/components/ImportForm_new/ApplicationSection/ApplicationSection.tsx b/src/components/ImportForm_new/ApplicationSection/ApplicationSection.tsx index aa57ef9d8..a85888c99 100644 --- a/src/components/ImportForm_new/ApplicationSection/ApplicationSection.tsx +++ b/src/components/ImportForm_new/ApplicationSection/ApplicationSection.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { Flex, FlexItem, FormSection } from '@patternfly/react-core'; import { useFormikContext } from 'formik'; -import { InputField } from 'formik-pf'; +import { InputField } from '../../../shared'; import { ApplicationThumbnail } from '../../ApplicationDetails/ApplicationThumbnail'; import { ImportFormValues } from '../type'; @@ -22,9 +22,8 @@ const ApplicationSection: React.FunctionComponent diff --git a/src/components/ImportForm_new/ComponentSection/ComponentSection.tsx b/src/components/ImportForm_new/ComponentSection/ComponentSection.tsx index f514e87a5..731f36b25 100644 --- a/src/components/ImportForm_new/ComponentSection/ComponentSection.tsx +++ b/src/components/ImportForm_new/ComponentSection/ComponentSection.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { FormSection, Text, TextContent, TextVariants } from '@patternfly/react-core'; -import { InputField } from 'formik-pf'; +import { InputField } from '../../../shared'; import { SourceSection } from './SourceSection'; export const ComponentSection = () => { @@ -13,7 +13,7 @@ export const ComponentSection = () => { - + ); }; diff --git a/src/components/ImportForm_new/ComponentSection/GitOptions.tsx b/src/components/ImportForm_new/ComponentSection/GitOptions.tsx index d76af5a7e..35cb32513 100644 --- a/src/components/ImportForm_new/ComponentSection/GitOptions.tsx +++ b/src/components/ImportForm_new/ComponentSection/GitOptions.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import { ExpandableSection } from '@patternfly/react-core'; -import { InputField } from 'formik-pf'; +import { ExpandableSection, FormSection, PageSection } from '@patternfly/react-core'; +import { InputField } from '../../../shared'; import HelpPopover from '../../HelpPopover'; const GitOptions: React.FC> = () => { @@ -9,22 +9,26 @@ const GitOptions: React.FC> = () => { toggleTextExpanded="Hide advanced Git options" toggleTextCollapsed="Show advanced Git options" > - + + + - - } - /> + + } + /> + + ); }; diff --git a/src/components/ImportForm_new/ComponentSection/SampleSection/SampleCard.tsx b/src/components/ImportForm_new/ComponentSection/SampleSection/SampleCard.tsx deleted file mode 100644 index 2f280740b..000000000 --- a/src/components/ImportForm_new/ComponentSection/SampleSection/SampleCard.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import * as React from 'react'; -import { CatalogItem } from '@openshift/dynamic-plugin-sdk-extensions'; -import { - Card, - CardHeader, - CardTitle, - CardBody, - CardFooter, - Badge, - ButtonVariant, - Button, - Divider, - TextContent, -} from '@patternfly/react-core'; -import { ArrowRightIcon } from '@patternfly/react-icons/dist/js/icons/arrow-right-icon'; -import ExternalLink from '../../../../shared/components/links/ExternalLink'; -import { SampleAttrs } from './useDevfileSamples'; - -type SampleCardProps = { - sample: CatalogItem; - onSampleImport: (url: string, name: string) => void; -}; - -const SampleCard: React.FC> = ({ - sample, - onSampleImport, -}) => { - const { icon, name, tags, description, attributes } = sample; - - const sourceUrl = (attributes as SampleAttrs)?.git?.remotes?.origin; - - const badges = tags?.map((tag) => ( - - {tag} - - )); - - const handleClick = React.useCallback(() => { - onSampleImport(sourceUrl, name); - }, [name, onSampleImport, sourceUrl]); - - return ( - - 0 && { - actions: { actions: <>{badges}, hasNoOffset: false, className: undefined }, - })} - > - {icon?.url && ( - {name} - )} - - {name} - - {description} - - - - - - - - - - ); -}; - -export default SampleCard; diff --git a/src/components/ImportForm_new/ComponentSection/SampleSection/SampleInfoAlert.tsx b/src/components/ImportForm_new/ComponentSection/SampleSection/SampleInfoAlert.tsx deleted file mode 100644 index 829811112..000000000 --- a/src/components/ImportForm_new/ComponentSection/SampleSection/SampleInfoAlert.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import * as React from 'react'; -import { Alert, AlertActionCloseButton } from '@patternfly/react-core'; - -const SAMPLES_FLOW_INSTRUCTION_KEY = 'samples-flow-instruction'; - -const SamplesInfoAlert: React.FC> = ({ children }) => { - const [showAlertInfo, setShowAlertInfo] = React.useState( - window.localStorage.getItem(SAMPLES_FLOW_INSTRUCTION_KEY) !== 'false', - ); - - return showAlertInfo ? ( - { - setShowAlertInfo(false); - window.localStorage.setItem(SAMPLES_FLOW_INSTRUCTION_KEY, 'false'); - }} - /> - } - > - {children} - - ) : null; -}; - -export default SamplesInfoAlert; diff --git a/src/components/ImportForm_new/ComponentSection/SampleSection/SampleSection.scss b/src/components/ImportForm_new/ComponentSection/SampleSection/SampleSection.scss deleted file mode 100644 index 1b02dd734..000000000 --- a/src/components/ImportForm_new/ComponentSection/SampleSection/SampleSection.scss +++ /dev/null @@ -1,9 +0,0 @@ -.hac-catalog { - margin: var(--pf-v5-global--spacer--lg); - --pf-v5-l-gallery--m-gutter--GridGap: var(--pf-v5-global--gutter--md); - - &__tile { - height: 100%; - color: black; - } -} diff --git a/src/components/ImportForm_new/ComponentSection/SampleSection/SampleSection.tsx b/src/components/ImportForm_new/ComponentSection/SampleSection/SampleSection.tsx deleted file mode 100644 index e83bd769e..000000000 --- a/src/components/ImportForm_new/ComponentSection/SampleSection/SampleSection.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import * as React from 'react'; -import { ExpandableSection, Gallery, GalleryItem } from '@patternfly/react-core'; -import { skeletonTileSelector } from '../../../../shared/components/catalog/utils/skeleton-catalog'; -import { StatusBox } from '../../../../shared/components/status-box/StatusBox'; -import SampleCard from './SampleCard'; -import { useDevfileSamples } from './useDevfileSamples'; - -import './SampleSection.scss'; - -type SampleSectionProp = { - onSampleImport?: (url: string, name: string) => void; -}; - -const SampleSection: React.FunctionComponent> = ({ - onSampleImport, -}) => { - const [samples, loaded, loadError] = useDevfileSamples(); - - const filteredSamples = React.useMemo( - () => (loaded ? samples.filter((item) => !item.attributes.deprecated) : []), - [samples, loaded], - ); - - return ( - - - {filteredSamples.length > 0 ? ( - - {filteredSamples.map((sample) => ( - - - - ))} - - ) : null} - - - ); -}; - -export default SampleSection; diff --git a/src/components/ImportForm_new/ComponentSection/SampleSection/__tests__/SampleSection.spec.tsx b/src/components/ImportForm_new/ComponentSection/SampleSection/__tests__/SampleSection.spec.tsx deleted file mode 100644 index f7f3754e5..000000000 --- a/src/components/ImportForm_new/ComponentSection/SampleSection/__tests__/SampleSection.spec.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import * as React from 'react'; -import { configure, fireEvent, render, screen, waitFor } from '@testing-library/react'; -import { useFormikContext } from 'formik'; -import { mockCatalogItem } from '../../../../../utils/__data__/mock-devfile-data'; -import SampleSection from '../SampleSection'; -import { useDevfileSamples } from '../useDevfileSamples'; - -import '@testing-library/jest-dom'; - -configure({ testIdAttribute: 'data-test' }); - -jest.mock('../../utils/useDevfileSamples', () => ({ - useDevfileSamples: jest.fn(), -})); - -jest.mock('formik', () => ({ - useFormikContext: jest.fn(), -})); - -jest.mock('react-i18next', () => ({ - useTranslation: jest.fn(() => ({ t: (x) => x })), -})); - -const onSampleImportMock = jest.fn(); - -const useFormikContextMock = useFormikContext as jest.Mock; - -const useDevfileSamplesMock = useDevfileSamples as jest.Mock; - -describe('SampleSection', () => { - it('renders component samples page with a progressbar when samples are loading', async () => { - useFormikContextMock.mockReturnValue({ - values: { source: { git: {} }, application: { name: '' } }, - setFieldValue: jest.fn(), - }); - useDevfileSamplesMock.mockReturnValue([[], false, null]); - render(); - await screen.getByRole('progressbar'); - }); - - it('renders component samples page with an empty state when no samples are loaded', async () => { - useFormikContextMock.mockReturnValue({ - values: { source: { git: {} }, application: { name: '' } }, - setFieldValue: jest.fn(), - }); - useDevfileSamplesMock.mockReturnValue([[], true, null]); - render(); - await waitFor(() => { - screen.getByText('No Catalog items found'); - }); - }); - - it('renders component samples page with nodejs sample tile', async () => { - useFormikContextMock.mockReturnValue({ - values: { source: { git: {} }, application: { name: '' } }, - setFieldValue: jest.fn(), - }); - useDevfileSamplesMock.mockReturnValue([[mockCatalogItem[0]], true, null]); - render(); - await waitFor(() => { - screen.getByText('Basic Node.js'); - }); - }); - - it('should call onSampleImport when user clicks on CTA', async () => { - const setFieldValue = jest.fn(); - useFormikContextMock.mockReturnValue({ - values: { source: { git: { url: 'https://github.com/repo' } }, application: 'test-app' }, - setFieldValue, - setStatus: jest.fn(), - }); - useDevfileSamplesMock.mockReturnValue([mockCatalogItem, true, null]); - - render(); - - await waitFor(() => fireEvent.click(screen.getByTestId('import-sample-Basic Node.js'))); - - await waitFor(() => { - expect(onSampleImportMock).toHaveBeenLastCalledWith( - 'https://github.com/nodeshift-starters/devfile-sample.git', - 'Basic Node.js', - ); - }); - }); - - it('should show empty state for filtered samples', async () => { - const setFieldValue = jest.fn(); - useFormikContextMock.mockReturnValue({ - values: { source: { git: { url: 'https://github.com/repo' } }, application: 'test-app' }, - setFieldValue, - setStatus: jest.fn(), - }); - useDevfileSamplesMock.mockReturnValue([mockCatalogItem, true, null]); - - render(); - - await waitFor(() => - fireEvent.input(screen.getByPlaceholderText('Filter by keyword...'), { - target: { value: 'asdf' }, - }), - ); - - await waitFor(() => screen.getByText('No results found')); - - await waitFor(() => fireEvent.click(screen.getByText('Clear all filters'))); - - await waitFor(() => screen.getByText('Basic Node.js')); - await waitFor(() => screen.getByText('Basic Quarkus')); - }); - - it('should filter sample items based on input value', async () => { - const setFieldValue = jest.fn(); - useFormikContextMock.mockReturnValue({ - values: { source: { git: { url: 'https://github.com/repo' } }, application: 'test-app' }, - setFieldValue, - setStatus: jest.fn(), - }); - useDevfileSamplesMock.mockReturnValue([mockCatalogItem, true, null]); - - render(); - - await waitFor(() => screen.getByText('Basic Node.js')); - await waitFor(() => screen.getByText('Basic Quarkus')); - - await waitFor(() => - fireEvent.input(screen.getByPlaceholderText('Filter by keyword...'), { - target: { value: 'node' }, - }), - ); - - await waitFor(() => screen.getByText('Basic Node.js')); - await waitFor(() => expect(screen.queryByText('Basic Quarkus')).not.toBeInTheDocument()); - }); - - it('should filter sample items based on tags', async () => { - const setFieldValue = jest.fn(); - useFormikContextMock.mockReturnValue({ - values: { source: { git: { url: 'https://github.com/repo' } }, application: 'test-app' }, - setFieldValue, - setStatus: jest.fn(), - }); - useDevfileSamplesMock.mockReturnValue([mockCatalogItem, true, null]); - - render(); - - await waitFor(() => screen.getByText('Basic Node.js')); - await waitFor(() => screen.getByText('Basic Quarkus')); - await waitFor(() => screen.getByText('Basic Python')); - await waitFor(() => screen.getByText('Basic Spring Boot')); - - await waitFor(() => - fireEvent.input(screen.getByPlaceholderText('Filter by keyword...'), { - target: { value: 'spring' }, - }), - ); - - await waitFor(() => screen.getByText('Basic Spring Boot')); - await waitFor(() => expect(screen.queryByText('Basic Node.js')).not.toBeInTheDocument()); - await waitFor(() => expect(screen.queryByText('Basic Quarkus')).not.toBeInTheDocument()); - await waitFor(() => expect(screen.queryByText('Basic Python')).not.toBeInTheDocument()); - }); -}); diff --git a/src/components/ImportForm_new/ComponentSection/SampleSection/useDevfileSamples.tsx b/src/components/ImportForm_new/ComponentSection/SampleSection/useDevfileSamples.tsx deleted file mode 100644 index e88b5e35e..000000000 --- a/src/components/ImportForm_new/ComponentSection/SampleSection/useDevfileSamples.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import * as React from 'react'; -import { CatalogItem } from '@openshift/dynamic-plugin-sdk-extensions'; -import { getDevfileSamples } from '../../../../utils/devfile-utils'; - -export type SampleAttrs = { - projectType: string; - language: string; - git: { - remotes: { - [remote: string]: string; - }; - }; - deprecated?: boolean; -}; - -export const useDevfileSamples = (): [CatalogItem[], boolean, string] => { - const [samples, setSamples] = React.useState[]>([]); - const [loaded, setLoaded] = React.useState(false); - const [loadError, setLoadError] = React.useState(); - - React.useEffect(() => { - let unmounted = false; - const fetchDevfileSamples = async () => { - if (unmounted) return; - - try { - const devfileSamples = await getDevfileSamples(); - - if (devfileSamples) { - setSamples(devfileSamples); - setLoaded(true); - } - } catch (e) { - setLoadError(`Failed to load devfile samples: ${e.message}`); - } - }; - - fetchDevfileSamples(); - return () => { - unmounted = true; - }; - }, []); - - return [samples, loaded, loadError]; -}; diff --git a/src/components/ImportForm_new/ComponentSection/SourceSection.tsx b/src/components/ImportForm_new/ComponentSection/SourceSection.tsx index bf32dec30..7dd124b39 100644 --- a/src/components/ImportForm_new/ComponentSection/SourceSection.tsx +++ b/src/components/ImportForm_new/ComponentSection/SourceSection.tsx @@ -1,22 +1,27 @@ import * as React from 'react'; -import { InputField } from 'formik-pf'; +import { ValidatedOptions } from '@patternfly/react-core'; +import { useField } from 'formik'; +import { InputField } from '../../../shared'; import GitOptions from './GitOptions'; export const SourceSection = () => { + const [, { touched, error }] = useField('source.git.url'); + const validated = touched + ? touched && !error + ? ValidatedOptions.success + : ValidatedOptions.error + : ValidatedOptions.default; return ( <> - + {validated === ValidatedOptions.success ? : null} ); }; diff --git a/src/components/ImportForm_new/GitImportActions.scss b/src/components/ImportForm_new/GitImportActions.scss index 08fec9596..809968f31 100644 --- a/src/components/ImportForm_new/GitImportActions.scss +++ b/src/components/ImportForm_new/GitImportActions.scss @@ -1,6 +1,6 @@ .git-import-actions { &__sticky { - position: fixed; + position: sticky; bottom: 0; width: 100%; padding-top: var(--pf-v5-global--spacer--lg); diff --git a/src/components/ImportForm_new/GitImportForm.tsx b/src/components/ImportForm_new/GitImportForm.tsx index cc0fdc84c..9ffdb9eb6 100644 --- a/src/components/ImportForm_new/GitImportForm.tsx +++ b/src/components/ImportForm_new/GitImportForm.tsx @@ -5,7 +5,9 @@ import ApplicationSection from './ApplicationSection/ApplicationSection'; import { ComponentSection } from './ComponentSection/ComponentSection'; import GitImportActions from './GitImportActions'; import { PipelineSection } from './PipelineSection/PipelineSection'; +import SecretSection from './SecretSection/SecretSection'; import { ImportFormValues } from './type'; +import { formValidationSchema } from './validation.utils'; export const GitImportForm: React.FC<{ applicationName: string }> = ({ applicationName }) => { const initialValues: ImportFormValues = { @@ -21,6 +23,8 @@ export const GitImportForm: React.FC<{ applicationName: string }> = ({ applicati pipeline: { name: '', }, + importSecrets: [], + newSecrets: [], }; const handleSubmit = React.useCallback(() => {}, []); @@ -31,6 +35,7 @@ export const GitImportForm: React.FC<{ applicationName: string }> = ({ applicati initialValues={initialValues} onSubmit={handleSubmit} onReset={handleReset} + validationSchema={formValidationSchema} > {(formikProps) => { return ( @@ -45,6 +50,7 @@ export const GitImportForm: React.FC<{ applicationName: string }> = ({ applicati <> + ) : null} diff --git a/src/components/ImportForm_new/PipelineSection/PipelineSection.tsx b/src/components/ImportForm_new/PipelineSection/PipelineSection.tsx index aad0c1c6e..a8e804876 100644 --- a/src/components/ImportForm_new/PipelineSection/PipelineSection.tsx +++ b/src/components/ImportForm_new/PipelineSection/PipelineSection.tsx @@ -1,24 +1,14 @@ import * as React from 'react'; -import { useFormikContext } from 'formik'; import { DropdownField } from '../../../shared'; -import { ImportFormValues } from '../type'; import { usePipelineTemplates } from './usePipelineTemplate'; export const PipelineSection: React.FunctionComponent = () => { const [template, loaded] = usePipelineTemplates(); - const { values, setFieldValue } = useFormikContext(); const dropdownItems = React.useMemo(() => { return loaded ? template.pipelines.map((t) => ({ key: t.name, value: t.name })) : []; }, [loaded, template?.pipelines]); - React.useEffect(() => { - if (loaded && values.pipeline.name) { - const bundle = template.pipelines.find((t) => t.name === values.pipeline.name)?.bundle; - setFieldValue('pipeline.bundle', bundle); - } - }, [loaded, setFieldValue, template?.pipelines, values.pipeline.name]); - return ( { + const [canCreateSecret] = useAccessReviewForModels(accessReviewResources); + const showModal = useModalLauncher(); + const { values, setFieldValue } = useFormikContext(); + const { namespace } = useWorkspaceInfo(); + + const [secrets, secretsLoaded] = useSecrets(namespace); + + const partnerTaskNames = getSupportedPartnerTaskSecrets().map(({ label }) => label); + const partnerTaskSecrets: string[] = + secrets && secretsLoaded + ? secrets + ?.filter((rs) => partnerTaskNames.includes(rs.metadata.name)) + ?.map((s) => s.metadata.name) || [] + : []; + + const onSubmit = React.useCallback( + (secretValue: any) => { + const allSecrets = [...values.importSecrets, secretValue]; + const secretNames = [...values.newSecrets, secretValue.secretName]; + setFieldValue('importSecrets', allSecrets); + setFieldValue('newSecrets', secretNames); + }, + [values, setFieldValue], + ); + + return ( + + + setFieldValue( + 'importSecrets', + values.importSecrets.filter((vs) => v.includes(vs.secretName)), + ) + } + > + {(props) => { + return ( + + + + + {props.removeButton} + + ); + }} + + } + onClick={() => + showModal(SecretModalLauncher([...partnerTaskSecrets, ...values.newSecrets], onSubmit)) + } + isDisabled={!canCreateSecret} + tooltip="You don't have access to add a secret" + > + Add secret + + + ); +}; +export default SecretSection; diff --git a/src/components/ImportForm_new/SecretSection/__tests__/SecretSection.spec.tsx b/src/components/ImportForm_new/SecretSection/__tests__/SecretSection.spec.tsx new file mode 100644 index 000000000..4728cda72 --- /dev/null +++ b/src/components/ImportForm_new/SecretSection/__tests__/SecretSection.spec.tsx @@ -0,0 +1,66 @@ +import * as React from 'react'; +import '@testing-library/jest-dom'; +import { useK8sWatchResource } from '@openshift/dynamic-plugin-sdk-utils'; +import { screen, fireEvent, act, waitFor } from '@testing-library/react'; +import { useAccessReviewForModels } from '../../../../utils/rbac'; +import { formikRenderer } from '../../../../utils/test-utils'; +import SecretSection from '../SecretSection'; + +jest.mock('@openshift/dynamic-plugin-sdk-utils', () => ({ + useK8sWatchResource: jest.fn(), + getActiveWorkspace: jest.fn(() => 'test-ws'), +})); + +jest.mock('../../../../utils/rbac', () => ({ + useAccessReviewForModels: jest.fn(), +})); + +const watchResourceMock = useK8sWatchResource as jest.Mock; +const accessReviewMock = useAccessReviewForModels as jest.Mock; + +describe('SecretSection', () => { + beforeEach(() => { + watchResourceMock.mockReturnValue([[], true]); + accessReviewMock.mockReturnValue([true, true]); + }); + + it('should render secret section', () => { + formikRenderer(, {}); + + screen.getByText('Secrets'); + screen.getByTestId('add-secret-button'); + }); + + it('should render added secrets in removable lists', () => { + formikRenderer(, { newSecrets: ['secret-one', 'secret-two'] }); + + expect(screen.queryByDisplayValue('secret-one')).toBeInTheDocument(); + expect(screen.queryByDisplayValue('secret-two')).toBeInTheDocument(); + }); + + it('should be able to remove the newly added secrets from the list', async () => { + formikRenderer(, { + importSecrets: [], + newSecrets: ['secret-one', 'secret-two'], + }); + + expect(screen.queryByDisplayValue('secret-one')).toBeInTheDocument(); + expect(screen.queryByDisplayValue('secret-two')).toBeInTheDocument(); + act(() => { + fireEvent.click(screen.getByTestId('newSecrets-1-remove-button')); + }); + + await waitFor(() => { + expect(screen.queryByDisplayValue('secret-two')).not.toBeInTheDocument(); + }); + }); + + it('should not allow adding secrets if user does not have create access', () => { + accessReviewMock.mockReturnValue([false, true]); + formikRenderer(, {}); + expect(screen.getByRole('button', { name: 'Add secret' })).toHaveAttribute( + 'aria-disabled', + 'true', + ); + }); +}); diff --git a/src/components/ImportForm_new/type.ts b/src/components/ImportForm_new/type.ts index d2dc52503..7a5d7047a 100644 --- a/src/components/ImportForm_new/type.ts +++ b/src/components/ImportForm_new/type.ts @@ -14,4 +14,16 @@ export type ImportFormValues = { name: string; bundle?: string; }; + importSecrets?: ImportSecret[]; + newSecrets?: string[]; +}; + +export type ImportSecret = { + secretName: string; + type: string; + keyValues: { + key: string; + value: string; + readOnlyKey?: boolean; + }[]; }; diff --git a/src/components/ImportForm_new/validation.utils.ts b/src/components/ImportForm_new/validation.utils.ts index cb4a87c39..0cf4da537 100644 --- a/src/components/ImportForm_new/validation.utils.ts +++ b/src/components/ImportForm_new/validation.utils.ts @@ -1,4 +1,5 @@ import * as yup from 'yup'; +import { ImportFormValues } from './type'; const gitUrlRegex = /^((((ssh|git|https?:?):\/\/:?)(([^\s@]+@|[^@]:?)[-\w.]+(:\d\d+:?)?(\/[-\w.~/?[\]!$&'()*+,;=:@%]*:?)?:?))|([^\s@]+@[-\w.]+:[-\w.~/?[\]!$&'()*+,;=:@%]*?:?))$/; @@ -10,12 +11,7 @@ const RESOURCE_NAME_REGEX_MSG = const MAX_RESOURCE_NAME_LENGTH = 63; const RESOURCE_NAME_LENGTH_ERROR_MSG = `Must be no more than ${MAX_RESOURCE_NAME_LENGTH} characters.`; -export const formValidationSchema = yup.object({ - application: yup - .string() - .matches(resourceNameRegex, RESOURCE_NAME_REGEX_MSG) - .max(MAX_RESOURCE_NAME_LENGTH, RESOURCE_NAME_LENGTH_ERROR_MSG) - .required('Required'), +const componentSchema = yup.object({ source: yup.object({ git: yup.object({ url: yup @@ -36,3 +32,17 @@ export const formValidationSchema = yup.object({ .required('Required'), pipeline: yup.object({ name: yup.string().required('Required') }), }); + +export const formValidationSchema = yup.mixed().test( + (values: ImportFormValues) => + yup + .object({ + application: yup + .string() + .matches(resourceNameRegex, RESOURCE_NAME_REGEX_MSG) + .max(MAX_RESOURCE_NAME_LENGTH, RESOURCE_NAME_LENGTH_ERROR_MSG) + .required('Required'), + }) + .concat(values.showComponent ? componentSchema : undefined) + .validate(values, { abortEarly: false }) as any, +); diff --git a/src/hooks/useSecrets.ts b/src/hooks/useSecrets.ts new file mode 100644 index 000000000..031831e4c --- /dev/null +++ b/src/hooks/useSecrets.ts @@ -0,0 +1,17 @@ +import React from 'react'; +import { useK8sWatchResource } from '@openshift/dynamic-plugin-sdk-utils'; +import { SecretGroupVersionKind } from '../models'; +import { SecretKind } from '../types'; + +export const useSecrets = (namespace: string): [SecretKind[], boolean, unknown] => { + const [secrets, loaded, error] = useK8sWatchResource({ + groupVersionKind: SecretGroupVersionKind, + namespace, + isList: true, + }); + + return React.useMemo( + () => [secrets.filter((rs) => !rs.metadata.deletionTimestamp), loaded, error], + [secrets, loaded, error], + ); +};