Skip to content

Commit

Permalink
Merge pull request #5061 from karthikjeeyar/container-source-form
Browse files Browse the repository at this point in the history
Add container source form in event source add flow
  • Loading branch information
openshift-merge-robot committed Apr 16, 2020
2 parents 08c9ebf + a822a22 commit de83364
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { FormFooter } from '@console/shared';
import { Form } from '@patternfly/react-core';
import AppSection from '@console/dev-console/src/components/import/app/AppSection';
import { FirehoseList } from '@console/dev-console/src/components/import/import-types';
import { useEventSourceList } from '../../utils/create-eventsources-utils';
import CronJobSection from './event-sources/CronJobSection';
import SinkBindingSection from './event-sources/SinkBindingSection';
import ApiServerSection from './event-sources/ApiServerSection';
Expand All @@ -13,7 +14,7 @@ import { EventSources } from './import-types';
import EventSourcesSelector from './event-sources/EventSourcesSelector';
import KafkaSourceSection from './event-sources/KafkaSourceSection';
import AdvancedSection from './AdvancedSection';
import { useEventSourceList } from '../../utils/create-eventsources-utils';
import ContainerSourceSection from './event-sources/ContainerSourceSection';

interface OwnProps {
namespace: string;
Expand All @@ -37,6 +38,7 @@ const EventSourceForm: React.FC<FormikProps<FormikValues> & OwnProps> = ({
{values.type === EventSources.SinkBinding && <SinkBindingSection />}
{values.type === EventSources.ApiServerSource && <ApiServerSection />}
{values.type === EventSources.KafkaSource && <KafkaSourceSection />}
{values.type === EventSources.ContainerSource && <ContainerSourceSection />}
<SinkSection namespace={namespace} />
<AppSection
project={values.project}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,28 @@ describe('Event Source ValidationUtils', () => {
});
});
});
describe('ContainerSource : Event Source Validation', () => {
it('should not throw error when the form data has valid values', async () => {
const ContainerSourceData = {
...getDefaultEventingData(EventSources.ContainerSource),
};
await eventSourceValidationSchema
.isValid(ContainerSourceData)
.then((valid) => expect(valid).toEqual(true));
});

it('should throw an error for required fields if empty', async () => {
const ContainerSourceData = {
...getDefaultEventingData(EventSources.ContainerSource),
};
ContainerSourceData.data.containersource.containers[0].image = '';
await eventSourceValidationSchema
.isValid(ContainerSourceData)
.then((valid) => expect(valid).toEqual(false));
await eventSourceValidationSchema.validate(ContainerSourceData).catch((err) => {
expect(err.message).toBe('Required');
expect(err.type).toBe('required');
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import * as React from 'react';
import * as _ from 'lodash';
import { useFormikContext, FormikValues } from 'formik';
import { TextInputTypes, FormGroup } from '@patternfly/react-core';
import { InputField, MultiColumnField } from '@console/shared';
import { AsyncComponent } from '@console/internal/components/utils';
import FormSection from '@console/dev-console/src/components/import/section/FormSection';
import { getSuggestedName } from '@console/dev-console/src/utils/imagestream-utils';

const containerPaths = {
Image: 'data.containersource.containers[0].image',
Name: 'data.containersource.containers[0].name',
Env: 'data.containersource.containers[0].env',
Args: 'data.containersource.containers[0].args',
};

const ContainerSourceSection: React.FC = () => {
const { values, setFieldValue } = useFormikContext<FormikValues>();
const {
data: {
containersource: {
containers: [{ env: envs, args }],
},
},
} = values;
const initialEnvValues = !_.isEmpty(envs) ? _.map(envs, (env) => _.values(env)) : [['', '']];
const [nameValue, setNameValue] = React.useState(initialEnvValues);
const handleNameValuePairs = React.useCallback(
({ nameValuePairs }) => {
const updatedNameValuePairs = _.compact(
nameValuePairs.map(([name, value]) => (value.length ? { name, value } : null)),
);
setNameValue(nameValuePairs);
setFieldValue(containerPaths.Env, updatedNameValuePairs);
},
[setFieldValue],
);
return (
<FormSection title="ContainerSource">
<h3 className="co-section-heading-tertiary">Container</h3>
<InputField
data-test-id="container-image-field"
type={TextInputTypes.text}
name={containerPaths.Image}
label="Image"
required
onChange={(e) => {
setFieldValue(containerPaths.Name, getSuggestedName(e.target.value));
}}
/>
<InputField
data-test-id="container-name-field"
type={TextInputTypes.text}
name={containerPaths.Name}
label="Name"
/>
<MultiColumnField
data-test-id="container-arg-field"
name={containerPaths.Args}
addLabel="Add args"
label="Arguments"
headers={[]}
emptyValues={{ name: '' }}
disableDeleteRow={args?.length === 1}
emptyMessage="No args are associated with the container."
>
<InputField name="name" type={TextInputTypes.text} placeholder="args" />
</MultiColumnField>
<FormGroup fieldId="containersource-env" label="Environment variables">
<AsyncComponent
loader={() =>
import('@console/internal/components/utils/name-value-editor').then(
(c) => c.NameValueEditor,
)
}
data-test-id="container-env-field"
nameValuePairs={nameValue}
valueString="Value"
nameString="Name"
readOnly={false}
allowSorting={false}
updateParentData={handleNameValuePairs}
/>
</FormGroup>
</FormSection>
);
};

export default ContainerSourceSection;
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import * as React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import { MultiColumnField } from '@console/shared';
import FormSection from '@console/dev-console/src/components/import/section/FormSection';
import { AsyncComponent } from '@console/internal/components/utils/async';
import ContainerSourceSection from '../ContainerSourceSection';
import { EventSources } from '../../import-types';

type ContainerSourceSectionProps = React.ComponentProps<typeof ContainerSourceSection>;

jest.mock('formik', () => ({
useField: jest.fn(() => [{}, {}]),
useFormikContext: jest.fn(() => ({
setFieldValue: jest.fn(),
setFieldTouched: jest.fn(),
validateForm: jest.fn(),
values: {
type: 'ContainerSource',
data: {
containersource: {
containers: [
{
args: [],
},
],
},
},
},
})),
}));
describe('ContainerSourceSection', () => {
let wrapper: ShallowWrapper<ContainerSourceSectionProps>;
beforeEach(() => {
wrapper = shallow(<ContainerSourceSection />);
});

it('should render ContainerSource FormSection', () => {
expect(wrapper.find(FormSection)).toHaveLength(1);
expect(wrapper.find(FormSection).props().title).toBe(EventSources.ContainerSource);
});

it('should render Container image and name input fields', () => {
const imageInputField = wrapper.find('[data-test-id="container-image-field"]');
const nameInputField = wrapper.find('[data-test-id="container-name-field"]');
expect(imageInputField).toHaveLength(1);
expect(nameInputField).toHaveLength(1);
});

it('should render Container args field', () => {
const argsField = wrapper.find(MultiColumnField);
expect(argsField).toHaveLength(1);
});

it('should render environment variables section', () => {
const nameValueEditorField = wrapper.find(AsyncComponent);
expect(nameValueEditorField).toHaveLength(1);
expect(nameValueEditorField.props().nameString).toBe('Name');
expect(nameValueEditorField.props().valueString).toBe('Value');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,19 @@ export const sourceDataSpecSchema = yup
topics: yup.string().required('Required'),
}),
}),
})
.when('type', {
is: EventSources.ContainerSource,
then: yup.object().shape({
containersource: yup.object().shape({
containers: yup.array().of(
yup.object({
image: yup.string().required('Required'),
}),
),
}),
}),
});

export const eventSourceValidationSchema = yup.object().shape({
project: projectNameValidationSchema,
application: applicationNameValidationSchema,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,16 @@ const eventSourceData = {
},
serviceAccountName: '',
},
containersource: {
containers: [
{
image: 'test-knative-image',
name: '',
args: [{ name: '' }],
env: [],
},
],
},
};

export const getDefaultEventingData = (typeEventSource: string): EventSourceFormData => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,33 @@ export const getKafkaSourceResource = (formData: EventSourceFormData): K8sResour
return _.merge({}, baseResource, kafkaSource);
};

export const getContainerSourceResource = (formData: EventSourceFormData): K8sResourceKind => {
const baseResource = _.omit(getEventSourcesDepResource(formData), ['spec.containers']);
const containersourceData = {
spec: {
template: {
spec: {
containers: _.map(formData.data.containersource?.containers, (container) => {
return {
image: container.image,
name: container.name,
args: container.args.map((arg) => arg.name),
env: container.env,
};
}),
},
},
},
};

return _.merge({}, baseResource, containersourceData);
};
export const getEventSourceResource = (formData: EventSourceFormData): K8sResourceKind => {
switch (formData.type) {
case EventSources.KafkaSource:
return getKafkaSourceResource(formData);
case EventSources.ContainerSource:
return getContainerSourceResource(formData);
default:
return getEventSourcesDepResource(formData);
}
Expand Down Expand Up @@ -130,6 +153,16 @@ export const getEventSourceData = (source: string) => {
},
serviceAccountName: '',
},
containersource: {
containers: [
{
image: '',
name: '',
args: [{ name: '' }],
env: [],
},
],
},
};
return eventSourceData[source];
};
Expand Down

0 comments on commit de83364

Please sign in to comment.