Skip to content

Commit

Permalink
Add create channel form via add flow
Browse files Browse the repository at this point in the history
  • Loading branch information
karthik committed Jul 17, 2020
1 parent dccfe8f commit bde2b5b
Show file tree
Hide file tree
Showing 20 changed files with 760 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,14 @@ export const EventSource: React.FC<Props> = ({

const createResources = (rawFormData: any): Promise<K8sResourceKind> => {
const knEventSourceResource = getEventSourceResource(rawFormData);
return k8sCreate(modelFor(referenceFor(knEventSourceResource)), knEventSourceResource);
if (knEventSourceResource?.kind && modelFor(referenceFor(knEventSourceResource))) {
return k8sCreate(modelFor(referenceFor(knEventSourceResource)), knEventSourceResource);
}
const errMessage =
knEventSourceResource?.kind && knEventSourceResource?.apiVersion
? `No model registered for ${referenceFor(knEventSourceResource)}`
: 'Invalid yaml';
return Promise.reject(new Error(errMessage));
};

const handleSubmit = (values, actions) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * as React from 'react';
import { Helmet } from 'react-helmet';
import { RouteComponentProps } from 'react-router';
import { PageBody, getBadgeFromType } from '@console/shared';
import { PageHeading } from '@console/internal/components/utils';
import NamespacedPage, {
NamespacedPageVariants,
} from '@console/dev-console/src/components/NamespacedPage';
import { QUERY_PROPERTIES } from '@console/dev-console/src/const';
import { KnativeEventingModel } from '../../models';
import { useChannelList } from '../../utils/create-channel-utils';
import AddChannel from './channels/AddChannel';

type EventingChannelPageProps = RouteComponentProps<{ ns?: string }>;

const EventingChannelPage: React.FC<EventingChannelPageProps> = ({ match, location }) => {
const namespace = match.params.ns;
const channels = useChannelList(namespace);
const searchParams = new URLSearchParams(location.search);
return (
<NamespacedPage disabled variant={NamespacedPageVariants.light}>
<Helmet>
<title>Channel</title>
</Helmet>
<PageHeading badge={getBadgeFromType(KnativeEventingModel.badge)} title="Channel">
Create a Knative Channel to create an event forwarding and persistence layer with in-memory
and reliable implementations
</PageHeading>
<PageBody flexLayout>
<AddChannel
namespace={namespace}
channels={channels}
selectedApplication={searchParams.get(QUERY_PROPERTIES.APPLICATION)}
contextSource={searchParams.get(QUERY_PROPERTIES.CONTEXT_SOURCE)}
/>
</PageBody>
</NamespacedPage>
);
};

export default EventingChannelPage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import * as React from 'react';
import { Formik } from 'formik';
import { connect } from 'react-redux';
import { history } from '@console/internal/components/utils';
import { getActiveApplication } from '@console/internal/reducers/ui';
import { RootState } from '@console/internal/redux';
import { ALL_APPLICATIONS_KEY } from '@console/shared';
import { K8sResourceKind, k8sCreate, modelFor, referenceFor } from '@console/internal/module/k8s';
import { sanitizeApplicationValue } from '@console/dev-console/src/utils/application-utils';
import { AddChannelFormData, ChannelListProps } from '../import-types';
import { addChannelValidationSchema } from '../eventSource-validation-utils';
import ChannelForm from './ChannelForm';
import { getCreateChannelResource } from '../../../utils/create-channel-utils';

interface ChannelProps {
namespace: string;
channels: ChannelListProps;
contextSource?: string;
selectedApplication?: string;
}

interface StateProps {
activeApplication: string;
}

type Props = ChannelProps & StateProps;

const AddChannel: React.FC<Props> = ({ namespace, channels, activeApplication }) => {
const initialValues: AddChannelFormData = {
application: {
initial: sanitizeApplicationValue(activeApplication),
name: sanitizeApplicationValue(activeApplication),
selectedKey: activeApplication,
},
name: '',
namespace,
apiVersion: '',
type: '',
data: {},
yamlData: '',
};

const createResources = (rawFormData: any): Promise<K8sResourceKind> => {
const channelResource = getCreateChannelResource(rawFormData);
if (channelResource?.kind && modelFor(referenceFor(channelResource))) {
return k8sCreate(modelFor(referenceFor(channelResource)), channelResource);
}
const errMessage =
channelResource?.kind && channelResource?.apiVersion
? `No model registered for ${referenceFor(channelResource)}`
: 'Invalid yaml';
return Promise.reject(new Error(errMessage));
};

const handleSubmit = (values, actions) => {
createResources(values)
.then(() => {
actions.setSubmitting(false);
history.push(`/topology/ns/${values.namespace}`);
})
.catch((err) => {
actions.setSubmitting(false);
actions.setStatus({ submitError: err.message });
});
};

return (
<Formik
initialValues={initialValues}
onSubmit={handleSubmit}
onReset={history.goBack}
validateOnBlur={false}
validateOnChange={false}
validationSchema={addChannelValidationSchema}
>
{(formikProps) => <ChannelForm {...formikProps} namespace={namespace} channels={channels} />}
</Formik>
);
};

const mapStateToProps = (state: RootState, ownProps: ChannelProps): StateProps => {
const activeApplication = ownProps.selectedApplication || getActiveApplication(state);
return {
activeApplication: activeApplication !== ALL_APPLICATIONS_KEY ? activeApplication : '',
};
};

export default connect(mapStateToProps)(AddChannel);
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import * as React from 'react';
import * as _ from 'lodash';
import { Alert } from '@patternfly/react-core';
import { FormikProps, FormikValues, useFormikContext } from 'formik';
import { FormFooter, FlexForm, useFormikValidationFix } from '@console/shared';
import { LoadingInline } from '@console/internal/components/utils';
import {
isDefaultChannel,
getChannelKind,
getChannelData,
useDefaultChannelConfiguration,
} from '../../../utils/create-channel-utils';
import { ChannelListProps } from '../import-types';
import FormViewSection from './sections/FormViewSection';
import ChannelSelector from './form-fields/ChannelSelector';
import ChannelYamlEditor from './form-fields/ChannelYamlEditor';

interface OwnProps {
namespace: string;
channels: ChannelListProps;
}

const ChannelForm: React.FC<FormikProps<FormikValues> & OwnProps> = ({
errors,
handleSubmit,
handleReset,
status,
isSubmitting,
dirty,
namespace,
channels,
}) => {
const {
values,
setFieldValue,
setFieldTouched,
validateForm,
setErrors,
setStatus,
} = useFormikContext<FormikValues>();
useFormikValidationFix(values);
const [defaultConfiguredChannel, defaultConfiguredChannelLoaded] = useDefaultChannelConfiguration(
namespace,
);
const channelHasFormView = values.type && isDefaultChannel(getChannelKind(values.type));
const channelKind = getChannelKind(values.type);
const onTypeChange = React.useCallback(
(item: string) => {
setErrors({});
setStatus({});
const kind = getChannelKind(item);
if (isDefaultChannel(kind)) {
const nameData = `data.${kind.toLowerCase()}`;
const sourceData = getChannelData(kind.toLowerCase());
setFieldValue(nameData, sourceData);
setFieldTouched(nameData, true);
}
setFieldValue('yamlData', '');
setFieldTouched('yamlData', true);

setFieldValue('type', item);
setFieldTouched('type', true);

setFieldValue('name', _.kebabCase(`${kind}`));
setFieldTouched('name', true);
validateForm();
},
[setErrors, setStatus, setFieldValue, setFieldTouched, validateForm],
);

return (
<FlexForm onSubmit={handleSubmit}>
{((channels && !channels.loaded) || !defaultConfiguredChannelLoaded) && <LoadingInline />}
{channels &&
channels.loaded &&
defaultConfiguredChannelLoaded &&
!_.isEmpty(channels.channelList) && (
<>
<ChannelSelector
channels={channels.channelList}
onChange={onTypeChange}
defaultConfiguredChannel={defaultConfiguredChannel}
defaultConfiguredChannelLoaded={defaultConfiguredChannelLoaded}
/>
{channelHasFormView && <FormViewSection namespace={namespace} kind={channelKind} />}
{!channelHasFormView && <ChannelYamlEditor />}
</>
)}
{channels && channels.loaded && _.isEmpty(channels.channelList) && (
<Alert variant="default" title="Channel cannot be created" isInline>
You do not have write access in this project.
</Alert>
)}
<FormFooter
handleReset={handleReset}
errorMessage={status && status.submitError}
isSubmitting={isSubmitting}
submitLabel="Create"
disableSubmit={!dirty || !_.isEmpty(errors)}
resetLabel="Cancel"
sticky
/>
</FlexForm>
);
};

export default ChannelForm;
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import * as React from 'react';
import * as _ from 'lodash';
import { useField } from 'formik';
import FormSection from '@console/dev-console/src/components/import/section/FormSection';
import { DropdownField, DropdownFieldProps } from '@console/shared';
import { getChannelKind } from '../../../../utils/create-channel-utils';
import { EventingChannelModel } from '../../../../models';

type ChannelSelectorProps = {
channels: string[];
defaultConfiguredChannel: string;
defaultConfiguredChannelLoaded: boolean;
} & Omit<DropdownFieldProps, 'name'>;

const ChannelSelector: React.FC<ChannelSelectorProps> = ({
channels,
onChange,
defaultConfiguredChannel,
defaultConfiguredChannelLoaded,
}) => {
const [selected] = useField('type');

const filteredChannels = _.chain(channels)
.filter((ch) => EventingChannelModel.kind !== getChannelKind(ch))
.partition((ref) => getChannelKind(ref) === defaultConfiguredChannel)
.flatten()
.value();

const channelData = filteredChannels.reduce((acc, channel) => {
const channelName = getChannelKind(channel);
acc[channel] =
channelName === defaultConfiguredChannel ? `${channelName} (Default)` : channelName;
return acc;
}, {});

const getDefaultChannel = React.useCallback((): string => {
return (
filteredChannels.find((ch) => getChannelKind(ch) === defaultConfiguredChannel) ||
filteredChannels[0]
);
}, [defaultConfiguredChannel, filteredChannels]);

React.useEffect(() => {
if (!selected.value && defaultConfiguredChannelLoaded && filteredChannels.length > 0) {
const channel = getDefaultChannel();
onChange && onChange(channel);
}
}, [
selected.value,
defaultConfiguredChannelLoaded,
getDefaultChannel,
onChange,
filteredChannels.length,
]);

return (
<FormSection extraMargin>
<DropdownField
name="type"
label="Type"
items={channelData}
title="Type"
onChange={onChange}
fullWidth
required
/>
</FormSection>
);
};

export default ChannelSelector;
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as React from 'react';
import { safeDump } from 'js-yaml';
import { YAMLEditorField } from '@console/shared';
import FormSection from '@console/dev-console/src/components/import/section/FormSection';
import { useFormikContext, FormikValues } from 'formik';
import {
isDefaultChannel,
getChannelKind,
getCreateChannelData,
} from '../../../../utils/create-channel-utils';
import { AddChannelFormData } from '../../import-types';

const ChannelYamlEditor: React.FC = () => {
const { values, setFieldValue, setFieldTouched } = useFormikContext<FormikValues>();
const formData = React.useRef(values);
if (formData.current.type !== values.type) {
formData.current = values;
}
React.useEffect(() => {
if (values.type && !isDefaultChannel(getChannelKind(values.type))) {
setFieldValue(
'yamlData',
safeDump(getCreateChannelData(formData.current as AddChannelFormData)),
);
setFieldTouched('yamlData', true);
}
}, [values.type, setFieldTouched, setFieldValue]);

return (
<FormSection flexLayout fullWidth>
<YAMLEditorField name="yamlData" />
</FormSection>
);
};

export default ChannelYamlEditor;
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as React from 'react';
import { TextInputTypes } from '@patternfly/react-core';
import ApplicationSelector from '@console/dev-console/src/components/import/app/ApplicationSelector';
import FormSection from '@console/dev-console/src/components/import/section/FormSection';
import { InputField } from '@console/shared';

type DefaultChannelSectionProps = {
namespace: string;
};

const DefaultChannelSection: React.FC<DefaultChannelSectionProps> = ({ namespace }) => (
<FormSection extraMargin>
<ApplicationSelector namespace={namespace} />
<InputField
type={TextInputTypes.text}
data-test-id="channel-name"
name="name"
label="Name"
helpText="A unique name for the component/channel"
required
/>
</FormSection>
);

export default DefaultChannelSection;

0 comments on commit bde2b5b

Please sign in to comment.