Create Schema Registry form (#209)

* First commit

* Create Schema Form. Refactoring

* Specs for Create Schema Registry form created

* Update thunks.spec.ts

* Update actions.spec.ts

Co-authored-by: Oleg Shuralev <workshur@gmail.com>
This commit is contained in:
Guzel738 2021-02-28 22:39:09 +03:00 committed by GitHub
parent 377fa830c6
commit 44cf449a8f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 656 additions and 261 deletions

View file

@ -15794,9 +15794,9 @@
"dev": true
},
"react-hook-form": {
"version": "6.15.1",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-6.15.1.tgz",
"integrity": "sha512-bL0LQuQ3OlM3JYfbacKtBPLOHhmgYz8Lj6ivMrvu2M6e1wnt4sbGRtPEPYCc/8z3WDbjrMwfAfLX92OsB65pFA=="
"version": "6.15.4",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-6.15.4.tgz",
"integrity": "sha512-K+Sw33DtTMengs8OdqFJI3glzNl1wBzSefD/ksQw/hJf9CnOHQAU6qy82eOrh0IRNt2G53sjr7qnnw1JDjvx1w=="
},
"react-is": {
"version": "17.0.1",

View file

@ -17,7 +17,7 @@
"react": "^17.0.1",
"react-datepicker": "^3.5.0",
"react-dom": "^17.0.1",
"react-hook-form": "^6.15.1",
"react-hook-form": "^6.15.4",
"react-json-tree": "^0.13.0",
"react-multi-select-component": "^2.0.14",
"react-redux": "^7.2.2",

View file

@ -55,14 +55,6 @@ const Details: React.FC<DetailsProps> = ({
</div>
</div>
<div className="level-right">
<button
className="button is-primary is-small level-item"
type="button"
title="in development"
disabled
>
Create Schema
</button>
<button
className="button is-warning is-small level-item"
type="button"

View file

@ -51,14 +51,6 @@ exports[`Details View Initial state matches snapshot 1`] = `
<div
className="level-right"
>
<button
className="button is-primary is-small level-item"
disabled={true}
title="in development"
type="button"
>
Create Schema
</button>
<button
className="button is-warning is-small level-item"
disabled={true}
@ -165,14 +157,6 @@ exports[`Details View when page with schema versions is loading matches snapshot
<div
className="level-right"
>
<button
className="button is-primary is-small level-item"
disabled={true}
title="in development"
type="button"
>
Create Schema
</button>
<button
className="button is-warning is-small level-item"
disabled={true}
@ -258,14 +242,6 @@ exports[`Details View when page with schema versions loaded when schema has vers
<div
className="level-right"
>
<button
className="button is-primary is-small level-item"
disabled={true}
title="in development"
type="button"
>
Create Schema
</button>
<button
className="button is-warning is-small level-item"
disabled={true}
@ -397,14 +373,6 @@ exports[`Details View when page with schema versions loaded when versions are em
<div
className="level-right"
>
<button
className="button is-primary is-small level-item"
disabled={true}
title="in development"
type="button"
>
Create Schema
</button>
<button
className="button is-warning is-small level-item"
disabled={true}

View file

@ -1,6 +1,8 @@
import React from 'react';
import { SchemaSubject } from 'generated-sources';
import Breadcrumb from '../../common/Breadcrumb/Breadcrumb';
import { NavLink, useParams } from 'react-router-dom';
import { clusterSchemaNewPath } from 'lib/paths';
import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
import ListItem from './ListItem';
export interface ListProps {
@ -8,9 +10,24 @@ export interface ListProps {
}
const List: React.FC<ListProps> = ({ schemas }) => {
const { clusterName } = useParams<{ clusterName: string }>();
return (
<div className="section">
<Breadcrumb>Schema Registry</Breadcrumb>
<div className="box">
<div className="level">
<div className="level-item level-right">
<NavLink
className="button is-primary"
to={clusterSchemaNewPath(clusterName)}
>
Create Schema
</NavLink>
</div>
</div>
</div>
<div className="box">
<table className="table is-striped is-fullwidth">
<thead>
@ -21,9 +38,15 @@ const List: React.FC<ListProps> = ({ schemas }) => {
</tr>
</thead>
<tbody>
{schemas.map((subject) => (
<ListItem key={subject.id} subject={subject} />
))}
{schemas.length > 0 ? (
schemas.map((subject) => (
<ListItem key={subject.id} subject={subject} />
))
) : (
<tr>
<td colSpan={10}>No schemas found</td>
</tr>
)}
</tbody>
</table>
</div>

View file

@ -1,6 +1,5 @@
import { connect } from 'react-redux';
import { RootState } from 'redux/interfaces';
import { withRouter } from 'react-router-dom';
import { getSchemaList } from 'redux/reducers/schemas/selectors';
import List from './List';
@ -8,4 +7,4 @@ const mapStateToProps = (state: RootState) => ({
schemas: getSchemaList(state),
});
export default withRouter(connect(mapStateToProps)(List));
export default connect(mapStateToProps)(List);

View file

@ -1,6 +1,7 @@
import React from 'react';
import { mount, shallow } from 'enzyme';
import { Provider } from 'react-redux';
import { shallow } from 'enzyme';
import { StaticRouter } from 'react-router';
import configureStore from 'redux/store/configureStore';
import ListContainer from '../ListContainer';
import List, { ListProps } from '../List';
@ -22,35 +23,31 @@ describe('List', () => {
});
describe('View', () => {
const pathname = `/ui/clusters/clusterName/schemas`;
const setupWrapper = (props: Partial<ListProps> = {}) => (
<List schemas={[]} {...props} />
<StaticRouter location={{ pathname }} context={{}}>
<List schemas={[]} {...props} />
</StaticRouter>
);
describe('without schemas', () => {
it('renders table heading without ListItem', () => {
const wrapper = shallow(setupWrapper());
const wrapper = mount(setupWrapper());
expect(wrapper.exists('Breadcrumb')).toBeTruthy();
expect(wrapper.exists('thead')).toBeTruthy();
expect(wrapper.exists('ListItem')).toBeFalsy();
});
it('matches snapshot', () => {
expect(shallow(setupWrapper())).toMatchSnapshot();
});
});
describe('with schemas', () => {
const wrapper = shallow(setupWrapper({ schemas }));
const wrapper = mount(setupWrapper({ schemas }));
it('renders table heading with ListItem', () => {
expect(wrapper.exists('Breadcrumb')).toBeTruthy();
expect(wrapper.exists('thead')).toBeTruthy();
expect(wrapper.find('ListItem').length).toEqual(3);
});
it('matches snapshot', () => {
expect(shallow(setupWrapper({ schemas }))).toMatchSnapshot();
});
});
});
});

View file

@ -1,102 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`List View with schemas matches snapshot 1`] = `
<div
className="section"
>
<Breadcrumb>
Schema Registry
</Breadcrumb>
<div
className="box"
>
<table
className="table is-striped is-fullwidth"
>
<thead>
<tr>
<th>
Schema Name
</th>
<th>
Version
</th>
<th>
Compatibility
</th>
</tr>
</thead>
<tbody>
<ListItem
key="1"
subject={
Object {
"compatibilityLevel": "BACKWARD",
"id": 1,
"schema": "{\\"type\\":\\"record\\",\\"name\\":\\"MyRecord1\\",\\"namespace\\":\\"com.mycompany\\",\\"fields\\":[{\\"name\\":\\"id\\",\\"type\\":\\"long\\"}]}",
"subject": "test",
"version": "1",
}
}
/>
<ListItem
key="1"
subject={
Object {
"compatibilityLevel": "BACKWARD",
"id": 1,
"schema": "{\\"type\\":\\"record\\",\\"name\\":\\"MyRecord2\\",\\"namespace\\":\\"com.mycompany\\",\\"fields\\":[{\\"name\\":\\"id\\",\\"type\\":\\"long\\"}]}",
"subject": "test2",
"version": "1",
}
}
/>
<ListItem
key="1"
subject={
Object {
"compatibilityLevel": "BACKWARD",
"id": 1,
"schema": "{\\"type\\":\\"record\\",\\"name\\":\\"MyRecord3\\",\\"namespace\\":\\"com.mycompany\\",\\"fields\\":[{\\"name\\":\\"id\\",\\"type\\":\\"long\\"}]}",
"subject": "test3",
"version": "1",
}
}
/>
</tbody>
</table>
</div>
</div>
`;
exports[`List View without schemas matches snapshot 1`] = `
<div
className="section"
>
<Breadcrumb>
Schema Registry
</Breadcrumb>
<div
className="box"
>
<table
className="table is-striped is-fullwidth"
>
<thead>
<tr>
<th>
Schema Name
</th>
<th>
Version
</th>
<th>
Compatibility
</th>
</tr>
</thead>
<tbody />
</table>
</div>
</div>
`;

View file

@ -12,7 +12,7 @@ export const schemas: SchemaSubject[] = [
{
subject: 'test2',
version: '1',
id: 1,
id: 2,
schema:
'{"type":"record","name":"MyRecord2","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
compatibilityLevel: 'BACKWARD',
@ -20,7 +20,7 @@ export const schemas: SchemaSubject[] = [
{
subject: 'test3',
version: '1',
id: 1,
id: 12,
schema:
'{"type":"record","name":"MyRecord3","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
compatibilityLevel: 'BACKWARD',

View file

@ -0,0 +1,115 @@
import React from 'react';
import { ClusterName, SchemaName, NewSchemaSubjectRaw } from 'redux/interfaces';
import { useForm } from 'react-hook-form';
import { ErrorMessage } from '@hookform/error-message';
import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb';
import { clusterSchemaPath, clusterSchemasPath } from 'lib/paths';
import { NewSchemaSubject } from 'generated-sources';
import { SCHEMA_NAME_VALIDATION_PATTERN } from 'lib/constants';
import { useHistory, useParams } from 'react-router';
export interface NewProps {
createSchema: (
clusterName: ClusterName,
subject: SchemaName,
newSchemaSubject: NewSchemaSubject
) => void;
}
const New: React.FC<NewProps> = ({ createSchema }) => {
const { clusterName } = useParams<{ clusterName: string }>();
const history = useHistory();
const {
register,
errors,
handleSubmit,
formState: { isDirty, isSubmitting },
} = useForm<NewSchemaSubjectRaw>();
const onSubmit = React.useCallback(
async ({ subject, schema }: NewSchemaSubjectRaw) => {
try {
await createSchema(clusterName, subject, { schema });
history.push(clusterSchemaPath(clusterName, subject));
} catch (e) {
// Show Error
}
},
[clusterName]
);
return (
<div className="section">
<div className="level">
<div className="level-item level-left">
<Breadcrumb
links={[
{
href: clusterSchemasPath(clusterName),
label: 'Schema Registry',
},
]}
>
New Schema
</Breadcrumb>
</div>
</div>
<div className="box">
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<div className="field">
<label className="label">Subject *</label>
<div className="control">
<input
className="input"
placeholder="Schema Name"
ref={register({
required: 'Topic Name is required.',
pattern: {
value: SCHEMA_NAME_VALIDATION_PATTERN,
message: 'Only alphanumeric, _, -, and . allowed',
},
})}
name="subject"
autoComplete="off"
disabled={isSubmitting}
/>
</div>
<p className="help is-danger">
<ErrorMessage errors={errors} name="subject" />
</p>
</div>
<div className="field">
<label className="label">Schema *</label>
<div className="control">
<textarea
className="textarea"
ref={register}
name="schema"
disabled={isSubmitting}
/>
</div>
<p className="help is-danger">
<ErrorMessage errors={errors} name="schema" />
</p>
</div>
</div>
<br />
<div className="field">
<div className="control">
<input
type="submit"
className="button is-primary"
disabled={isSubmitting || !isDirty}
/>
</div>
</div>
</form>
</div>
</div>
);
};
export default New;

View file

@ -0,0 +1,15 @@
import { connect } from 'react-redux';
import { RootState } from 'redux/interfaces';
import { createSchema } from 'redux/actions';
import { getSchemaCreated } from 'redux/reducers/schemas/selectors';
import New from './New';
const mapStateToProps = (state: RootState) => ({
isSchemaCreated: getSchemaCreated(state),
});
const mapDispatchToProps = {
createSchema,
};
export default connect(mapStateToProps, mapDispatchToProps)(New);

View file

@ -0,0 +1,37 @@
import React from 'react';
import configureStore from 'redux/store/configureStore';
import { mount, shallow } from 'enzyme';
import { Provider } from 'react-redux';
import { StaticRouter } from 'react-router-dom';
import NewContainer from '../NewContainer';
import New, { NewProps } from '../New';
describe('New', () => {
describe('Container', () => {
const store = configureStore();
it('renders view', () => {
const component = shallow(
<Provider store={store}>
<NewContainer />
</Provider>
);
expect(component.exists()).toBeTruthy();
});
});
describe('View', () => {
const pathname = '/ui/clusters/clusterName/schemas/new';
const setupWrapper = (props: Partial<NewProps> = {}) => (
<StaticRouter location={{ pathname }} context={{}}>
<New createSchema={jest.fn()} {...props} />
</StaticRouter>
);
it('matches snapshot', () => {
expect(mount(setupWrapper())).toMatchSnapshot();
});
});
});

View file

@ -0,0 +1,189 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`New View matches snapshot 1`] = `
<StaticRouter
context={Object {}}
location={
Object {
"pathname": "/ui/clusters/clusterName/schemas/new",
}
}
>
<Router
history={
Object {
"action": "POP",
"block": [Function],
"createHref": [Function],
"go": [Function],
"goBack": [Function],
"goForward": [Function],
"listen": [Function],
"location": Object {
"hash": "",
"pathname": "/ui/clusters/clusterName/schemas/new",
"search": "",
},
"push": [Function],
"replace": [Function],
}
}
staticContext={Object {}}
>
<New
createSchema={[MockFunction]}
>
<div
className="section"
>
<div
className="level"
>
<div
className="level-item level-left"
>
<Breadcrumb
links={
Array [
Object {
"href": "/ui/clusters/undefined/schemas",
"label": "Schema Registry",
},
]
}
>
<nav
aria-label="breadcrumbs"
className="breadcrumb"
>
<ul>
<li
key="/ui/clusters/undefined/schemas"
>
<NavLink
to="/ui/clusters/undefined/schemas"
>
<Link
aria-current={null}
to={
Object {
"hash": "",
"pathname": "/ui/clusters/undefined/schemas",
"search": "",
"state": null,
}
}
>
<LinkAnchor
aria-current={null}
href="/ui/clusters/undefined/schemas"
navigate={[Function]}
>
<a
aria-current={null}
href="/ui/clusters/undefined/schemas"
onClick={[Function]}
>
Schema Registry
</a>
</LinkAnchor>
</Link>
</NavLink>
</li>
<li
className="is-active"
>
<span
className=""
>
New Schema
</span>
</li>
</ul>
</nav>
</Breadcrumb>
</div>
</div>
<div
className="box"
>
<form
onSubmit={[Function]}
>
<div>
<div
className="field"
>
<label
className="label"
>
Subject *
</label>
<div
className="control"
>
<input
autoComplete="off"
className="input"
disabled={false}
name="subject"
placeholder="Schema Name"
/>
</div>
<p
className="help is-danger"
>
<Component
errors={Object {}}
name="subject"
/>
</p>
</div>
<div
className="field"
>
<label
className="label"
>
Schema *
</label>
<div
className="control"
>
<textarea
className="textarea"
disabled={false}
name="schema"
/>
</div>
<p
className="help is-danger"
>
<Component
errors={Object {}}
name="schema"
/>
</p>
</div>
</div>
<br />
<div
className="field"
>
<div
className="control"
>
<input
className="button is-primary"
disabled={true}
type="submit"
/>
</div>
</div>
</form>
</div>
</div>
</New>
</Router>
</StaticRouter>
`;

View file

@ -1,43 +1,49 @@
import React from 'react';
import { ClusterName } from 'redux/interfaces';
import { Switch, Route } from 'react-router-dom';
import { Switch, Route, useParams } from 'react-router-dom';
import PageLoader from 'components/common/PageLoader/PageLoader';
import ListContainer from './List/ListContainer';
import DetailsContainer from './Details/DetailsContainer';
import NewContainer from './New/NewContainer';
export interface SchemasProps {
isFetched: boolean;
clusterName: ClusterName;
isFetching: boolean;
fetchSchemasByClusterName: (clusterName: ClusterName) => void;
}
const Schemas: React.FC<SchemasProps> = ({
isFetched,
isFetching,
fetchSchemasByClusterName,
clusterName,
}) => {
const { clusterName } = useParams<{ clusterName: string }>();
React.useEffect(() => {
fetchSchemasByClusterName(clusterName);
}, [fetchSchemasByClusterName, clusterName]);
if (isFetched) {
return (
<Switch>
<Route
exact
path="/ui/clusters/:clusterName/schemas"
component={ListContainer}
/>
<Route
exact
path="/ui/clusters/:clusterName/schemas/:subject/latest"
component={DetailsContainer}
/>
</Switch>
);
if (isFetching) {
return <PageLoader />;
}
return <PageLoader />;
return (
<Switch>
<Route
exact
path="/ui/clusters/:clusterName/schemas"
component={ListContainer}
/>
<Route
exact
path="/ui/clusters/:clusterName/schemas/new"
component={NewContainer}
/>
<Route
exact
path="/ui/clusters/:clusterName/schemas/:subject/latest"
component={DetailsContainer}
/>
</Switch>
);
};
export default Schemas;

View file

@ -1,32 +1,15 @@
import { connect } from 'react-redux';
import { ClusterName, RootState } from 'redux/interfaces';
import { RootState } from 'redux/interfaces';
import { fetchSchemasByClusterName } from 'redux/actions';
import { getIsSchemaListFetched } from 'redux/reducers/schemas/selectors';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { getIsSchemaListFetching } from 'redux/reducers/schemas/selectors';
import Schemas from './Schemas';
interface RouteProps {
clusterName: ClusterName;
}
type OwnProps = RouteComponentProps<RouteProps>;
const mapStateToProps = (
state: RootState,
{
match: {
params: { clusterName },
},
}: OwnProps
) => ({
isFetched: getIsSchemaListFetched(state),
clusterName,
const mapStateToProps = (state: RootState) => ({
isFetching: getIsSchemaListFetching(state),
});
const mapDispatchToProps = {
fetchSchemasByClusterName,
};
export default withRouter(
connect(mapStateToProps, mapDispatchToProps)(Schemas)
);
export default connect(mapStateToProps, mapDispatchToProps)(Schemas);

View file

@ -1,10 +1,8 @@
import React from 'react';
import { Provider } from 'react-redux';
import { shallow } from 'enzyme';
import { mount } from 'enzyme';
import configureStore from 'redux/store/configureStore';
import { StaticRouter } from 'react-router-dom';
import { match } from 'react-router';
import { ClusterName } from 'redux/interfaces';
import Schemas, { SchemasProps } from '../Schemas';
import SchemasContainer from '../SchemasContainer';
@ -15,7 +13,7 @@ describe('Schemas', () => {
const store = configureStore();
it('renders view', () => {
const component = shallow(
const component = mount(
<Provider store={store}>
<StaticRouter location={{ pathname }} context={{}}>
<SchemasContainer />
@ -28,12 +26,13 @@ describe('Schemas', () => {
describe('View', () => {
const setupWrapper = (props: Partial<SchemasProps> = {}) => (
<Schemas
isFetched
clusterName="Test"
fetchSchemasByClusterName={jest.fn()}
{...props}
/>
<StaticRouter location={{ pathname }} context={{}}>
<Schemas
isFetching
fetchSchemasByClusterName={jest.fn()}
{...props}
/>
</StaticRouter>
);
describe('Initial state', () => {
let useEffect: jest.SpyInstance<
@ -43,7 +42,6 @@ describe('Schemas', () => {
deps?: React.DependencyList | undefined
]
>;
let wrapper;
const mockedFn = jest.fn();
const mockedUseEffect = () => {
@ -53,33 +51,20 @@ describe('Schemas', () => {
beforeEach(() => {
useEffect = jest.spyOn(React, 'useEffect');
mockedUseEffect();
wrapper = shallow(
setupWrapper({ fetchSchemasByClusterName: mockedFn })
);
});
it('should call fetchSchemasByClusterName every render', () => {
mount(setupWrapper({ fetchSchemasByClusterName: mockedFn }));
expect(mockedFn).toHaveBeenCalled();
});
it('matches snapshot', () => {
expect(
shallow(setupWrapper({ fetchSchemasByClusterName: mockedFn }))
).toMatchSnapshot();
});
});
describe('when page is loading', () => {
const wrapper = shallow(setupWrapper({ isFetched: false }));
const wrapper = mount(setupWrapper({ isFetching: true }));
it('renders PageLoader', () => {
expect(wrapper.exists('PageLoader')).toBeTruthy();
});
it('matches snapshot', () => {
expect(shallow(setupWrapper({ isFetched: false }))).toMatchSnapshot();
});
});
});
});

View file

@ -1,18 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Schemas Container View Initial state matches snapshot 1`] = `
<Switch>
<Route
component={[Function]}
exact={true}
path="/ui/clusters/:clusterName/schemas"
/>
<Route
component={[Function]}
exact={true}
path="/ui/clusters/:clusterName/schemas/:subject/latest"
/>
</Switch>
`;
exports[`Schemas Container View when page is loading matches snapshot 1`] = `<PageLoader />`;

View file

@ -9,6 +9,7 @@ export const BASE_PARAMS: ConfigurationParameters = {
};
export const TOPIC_NAME_VALIDATION_PATTERN = RegExp(/^[.,A-Za-z0-9_-]+$/);
export const SCHEMA_NAME_VALIDATION_PATTERN = RegExp(/^[.,A-Za-z0-9_-]+$/);
export const MILLISECONDS_IN_WEEK = 604_800_000;
export const MILLISECONDS_IN_DAY = 86_400_000;

View file

@ -1,4 +1,4 @@
import { ClusterName, TopicName } from 'redux/interfaces';
import { ClusterName, SchemaName, TopicName } from 'redux/interfaces';
const clusterPath = (clusterName: ClusterName) => `/ui/clusters/${clusterName}`;
@ -12,6 +12,8 @@ export const clusterConsumerGroupsPath = (clusterName: ClusterName) =>
`${clusterPath(clusterName)}/consumer-groups`;
export const clusterSchemasPath = (clusterName: ClusterName) =>
`${clusterPath(clusterName)}/schemas`;
export const clusterSchemaNewPath = (clusterName: ClusterName) =>
`${clusterPath(clusterName)}/schemas/new`;
export const clusterTopicPath = (
clusterName: ClusterName,
@ -30,3 +32,8 @@ export const clusterTopicsTopicEditPath = (
clusterName: ClusterName,
topicName: TopicName
) => `${clusterTopicsPath(clusterName)}/${topicName}/edit`;
export const clusterSchemaPath = (
clusterName: ClusterName,
subject: SchemaName
) => `${clusterSchemasPath(clusterName)}/${subject}/latest`;

View file

@ -6,13 +6,13 @@ import * as actions from '../actions';
describe('Actions', () => {
describe('fetchClusterStatsAction', () => {
it('creates an REQUEST action', () => {
it('creates a REQUEST action', () => {
expect(actions.fetchClusterStatsAction.request()).toEqual({
type: 'GET_CLUSTER_STATUS__REQUEST',
});
});
it('creates an SUCCESS action', () => {
it('creates a SUCCESS action', () => {
expect(
actions.fetchClusterStatsAction.success({ brokerCount: 1 })
).toEqual({
@ -23,7 +23,7 @@ describe('Actions', () => {
});
});
it('creates an FAILURE action', () => {
it('creates a FAILURE action', () => {
expect(actions.fetchClusterStatsAction.failure()).toEqual({
type: 'GET_CLUSTER_STATUS__FAILURE',
});
@ -75,4 +75,27 @@ describe('Actions', () => {
});
});
});
describe('createSchemaAction', () => {
it('creates a REQUEST action', () => {
expect(actions.createSchemaAction.request()).toEqual({
type: 'POST_SCHEMA__REQUEST',
});
});
it('creates a SUCCESS action', () => {
expect(
actions.createSchemaAction.success(schemaVersionsPayload[0])
).toEqual({
type: 'POST_SCHEMA__SUCCESS',
payload: schemaVersionsPayload[0],
});
});
it('creates a FAILURE action', () => {
expect(actions.createSchemaAction.failure()).toEqual({
type: 'POST_SCHEMA__FAILURE',
});
});
});
});

View file

@ -1,4 +1,4 @@
import { ClusterStats } from 'generated-sources';
import { ClusterStats, NewSchemaSubject } from 'generated-sources';
export const clusterStats: ClusterStats = {
brokerCount: 1,
@ -11,3 +11,8 @@ export const clusterStats: ClusterStats = {
underReplicatedPartitionCount: 0,
diskUsage: [{ brokerId: 1, segmentSize: 6538, segmentCount: 6 }],
};
export const schemaPayload: NewSchemaSubject = {
schema:
'{"type":"record","name":"MyRecord1","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
};

View file

@ -105,4 +105,36 @@ describe('Thunks', () => {
]);
});
});
describe('createSchema', () => {
it('creates POST_SCHEMA__SUCCESS when posting new schema', async () => {
fetchMock.postOnce(`/api/clusters/${clusterName}/schemas/${subject}`, {
body: schemaFixtures.schemaVersionsPayload[0],
});
await store.dispatch(
thunks.createSchema(clusterName, subject, fixtures.schemaPayload)
);
expect(store.getActions()).toEqual([
actions.createSchemaAction.request(),
actions.createSchemaAction.success(
schemaFixtures.schemaVersionsPayload[0]
),
]);
});
// it('creates POST_SCHEMA__FAILURE when posting new schema', async () => {
// fetchMock.postOnce(
// `/api/clusters/${clusterName}/schemas/${subject}`,
// 404
// );
// await store.dispatch(
// thunks.createSchema(clusterName, subject, fixtures.schemaPayload)
// );
// expect(store.getActions()).toEqual([
// actions.createSchemaAction.request(),
// actions.createSchemaAction.failure(),
// ]);
// expect(store.getActions()).toThrow();
// });
});
});

View file

@ -109,3 +109,9 @@ export const fetchSchemaVersionsAction = createAsyncAction(
'GET_SCHEMA_VERSIONS__SUCCESS',
'GET_SCHEMA_VERSIONS__FAILURE'
)<undefined, SchemaSubject[], undefined>();
export const createSchemaAction = createAsyncAction(
'POST_SCHEMA__REQUEST',
'POST_SCHEMA__SUCCESS',
'POST_SCHEMA__FAILURE'
)<undefined, SchemaSubject, undefined>();

View file

@ -5,6 +5,8 @@ import {
Topic,
TopicFormData,
TopicConfig,
NewSchemaSubject,
SchemaSubject,
} from 'generated-sources';
import {
ConsumerGroupID,
@ -280,3 +282,22 @@ export const fetchSchemaVersions = (
dispatch(actions.fetchSchemaVersionsAction.failure());
}
};
export const createSchema = (
clusterName: ClusterName,
subject: SchemaName,
newSchemaSubject: NewSchemaSubject
): PromiseThunkResult => async (dispatch) => {
dispatch(actions.createSchemaAction.request());
try {
const schema: SchemaSubject = await apiClient.createNewSchema({
clusterName,
subject,
newSchemaSubject,
});
dispatch(actions.createSchemaAction.success(schema));
} catch (e) {
dispatch(actions.createSchemaAction.failure());
throw e;
}
};

View file

@ -1,4 +1,4 @@
import { SchemaSubject } from 'generated-sources';
import { NewSchemaSubject, SchemaSubject } from 'generated-sources';
export type SchemaName = string;
@ -7,3 +7,7 @@ export interface SchemasState {
allNames: SchemaName[];
currentSchemaVersions: SchemaSubject[];
}
export interface NewSchemaSubjectRaw extends NewSchemaSubject {
subject: string;
}

View file

@ -56,3 +56,21 @@ Object {
],
}
`;
exports[`Schemas reducer reacts on POST_SCHEMA__SUCCESS and returns payload 1`] = `
Object {
"allNames": Array [
"test",
],
"byName": Object {
"test": Object {
"compatibilityLevel": "BACKWARD",
"id": 1,
"schema": "{\\"type\\":\\"record\\",\\"name\\":\\"MyRecord1\\",\\"namespace\\":\\"com.mycompany\\",\\"fields\\":[{\\"name\\":\\"id\\",\\"type\\":\\"long\\"}]}",
"subject": "test",
"version": "1",
},
},
"currentSchemaVersions": Array [],
}
`;

View file

@ -52,3 +52,47 @@ export const schemaVersionsPayload: SchemaSubject[] = [
compatibilityLevel: 'BACKWARD',
},
];
export const newSchemaPayload: SchemaSubject = {
subject: 'test4',
version: '2',
id: 2,
schema:
'{"type":"record","name":"MyRecord4","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
compatibilityLevel: 'BACKWARD',
};
export const clusterSchemasPayloadWithNewSchema: SchemaSubject[] = [
{
subject: 'test2',
version: '3',
id: 4,
schema:
'{"type":"record","name":"MyRecord4","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
compatibilityLevel: 'BACKWARD',
},
{
subject: 'test3',
version: '1',
id: 5,
schema:
'{"type":"record","name":"MyRecord","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
compatibilityLevel: 'BACKWARD',
},
{
subject: 'test',
version: '2',
id: 2,
schema:
'{"type":"record","name":"MyRecord2","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
compatibilityLevel: 'BACKWARD',
},
{
subject: 'test4',
version: '2',
id: 2,
schema:
'{"type":"record","name":"MyRecord4","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
compatibilityLevel: 'BACKWARD',
},
];

View file

@ -1,4 +1,5 @@
import {
createSchemaAction,
fetchSchemasByClusterNameAction,
fetchSchemaVersionsAction,
} from 'redux/actions';
@ -17,6 +18,9 @@ describe('Schemas reducer', () => {
expect(reducer(undefined, fetchSchemaVersionsAction.request())).toEqual(
initialState
);
expect(reducer(undefined, createSchemaAction.request())).toEqual(
initialState
);
});
it('reacts on GET_CLUSTER_SCHEMAS__SUCCESS and returns payload', () => {
@ -36,4 +40,10 @@ describe('Schemas reducer', () => {
)
).toMatchSnapshot();
});
it('reacts on POST_SCHEMA__SUCCESS and returns payload', () => {
expect(
reducer(undefined, createSchemaAction.success(schemaVersionsPayload[0]))
).toMatchSnapshot();
});
});

View file

@ -1,10 +1,16 @@
import {
createSchemaAction,
fetchSchemasByClusterNameAction,
fetchSchemaVersionsAction,
} from 'redux/actions';
import configureStore from 'redux/store/configureStore';
import * as selectors from '../selectors';
import { clusterSchemasPayload, schemaVersionsPayload } from './fixtures';
import {
clusterSchemasPayload,
clusterSchemasPayloadWithNewSchema,
newSchemaPayload,
schemaVersionsPayload,
} from './fixtures';
const store = configureStore();
@ -13,6 +19,7 @@ describe('Schemas selectors', () => {
it('returns fetch status', () => {
expect(selectors.getIsSchemaListFetched(store.getState())).toBeFalsy();
expect(selectors.getIsSchemaVersionFetched(store.getState())).toBeFalsy();
expect(selectors.getSchemaCreated(store.getState())).toBeFalsy();
});
it('returns schema list', () => {
@ -34,6 +41,7 @@ describe('Schemas selectors', () => {
fetchSchemasByClusterNameAction.success(clusterSchemasPayload)
);
store.dispatch(fetchSchemaVersionsAction.success(schemaVersionsPayload));
store.dispatch(createSchemaAction.success(newSchemaPayload));
});
it('returns fetch status', () => {
@ -41,11 +49,12 @@ describe('Schemas selectors', () => {
expect(
selectors.getIsSchemaVersionFetched(store.getState())
).toBeTruthy();
expect(selectors.getSchemaCreated(store.getState())).toBeTruthy();
});
it('returns schema list', () => {
expect(selectors.getSchemaList(store.getState())).toEqual(
clusterSchemasPayload
clusterSchemasPayloadWithNewSchema
);
});

View file

@ -32,12 +32,26 @@ const updateSchemaList = (
}, initialMemo);
};
const addToSchemaList = (
state: SchemasState,
payload: SchemaSubject
): SchemasState => {
const newState: SchemasState = {
...state,
};
newState.allNames.push(payload.subject as string);
newState.byName[payload.subject as string] = { ...payload };
return newState;
};
const reducer = (state = initialState, action: Action): SchemasState => {
switch (action.type) {
case 'GET_CLUSTER_SCHEMAS__SUCCESS':
return updateSchemaList(state, action.payload);
case 'GET_SCHEMA_VERSIONS__SUCCESS':
return { ...state, currentSchemaVersions: action.payload };
case 'POST_SCHEMA__SUCCESS':
return addToSchemaList(state, action.payload);
default:
return state;
}

View file

@ -15,16 +15,28 @@ const getSchemaVersionsFetchingStatus = createFetchingSelector(
'GET_SCHEMA_VERSIONS'
);
const getSchemaCreationStatus = createFetchingSelector('POST_SCHEMA');
export const getIsSchemaListFetched = createSelector(
getSchemaListFetchingStatus,
(status) => status === 'fetched'
);
export const getIsSchemaListFetching = createSelector(
getSchemaListFetchingStatus,
(status) => status === 'fetching' || status === 'notFetched'
);
export const getIsSchemaVersionFetched = createSelector(
getSchemaVersionsFetchingStatus,
(status) => status === 'fetched'
);
export const getSchemaCreated = createSelector(
getSchemaCreationStatus,
(status) => status === 'fetched'
);
export const getSchemaList = createSelector(
getIsSchemaListFetched,
getAllNames,