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:
parent
377fa830c6
commit
44cf449a8f
31 changed files with 656 additions and 261 deletions
6
kafka-ui-react-app/package-lock.json
generated
6
kafka-ui-react-app/package-lock.json
generated
|
@ -15794,9 +15794,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"react-hook-form": {
|
"react-hook-form": {
|
||||||
"version": "6.15.1",
|
"version": "6.15.4",
|
||||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-6.15.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-6.15.4.tgz",
|
||||||
"integrity": "sha512-bL0LQuQ3OlM3JYfbacKtBPLOHhmgYz8Lj6ivMrvu2M6e1wnt4sbGRtPEPYCc/8z3WDbjrMwfAfLX92OsB65pFA=="
|
"integrity": "sha512-K+Sw33DtTMengs8OdqFJI3glzNl1wBzSefD/ksQw/hJf9CnOHQAU6qy82eOrh0IRNt2G53sjr7qnnw1JDjvx1w=="
|
||||||
},
|
},
|
||||||
"react-is": {
|
"react-is": {
|
||||||
"version": "17.0.1",
|
"version": "17.0.1",
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-datepicker": "^3.5.0",
|
"react-datepicker": "^3.5.0",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
"react-hook-form": "^6.15.1",
|
"react-hook-form": "^6.15.4",
|
||||||
"react-json-tree": "^0.13.0",
|
"react-json-tree": "^0.13.0",
|
||||||
"react-multi-select-component": "^2.0.14",
|
"react-multi-select-component": "^2.0.14",
|
||||||
"react-redux": "^7.2.2",
|
"react-redux": "^7.2.2",
|
||||||
|
|
|
@ -55,14 +55,6 @@ const Details: React.FC<DetailsProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="level-right">
|
<div className="level-right">
|
||||||
<button
|
|
||||||
className="button is-primary is-small level-item"
|
|
||||||
type="button"
|
|
||||||
title="in development"
|
|
||||||
disabled
|
|
||||||
>
|
|
||||||
Create Schema
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
className="button is-warning is-small level-item"
|
className="button is-warning is-small level-item"
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
@ -51,14 +51,6 @@ exports[`Details View Initial state matches snapshot 1`] = `
|
||||||
<div
|
<div
|
||||||
className="level-right"
|
className="level-right"
|
||||||
>
|
>
|
||||||
<button
|
|
||||||
className="button is-primary is-small level-item"
|
|
||||||
disabled={true}
|
|
||||||
title="in development"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
Create Schema
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
className="button is-warning is-small level-item"
|
className="button is-warning is-small level-item"
|
||||||
disabled={true}
|
disabled={true}
|
||||||
|
@ -165,14 +157,6 @@ exports[`Details View when page with schema versions is loading matches snapshot
|
||||||
<div
|
<div
|
||||||
className="level-right"
|
className="level-right"
|
||||||
>
|
>
|
||||||
<button
|
|
||||||
className="button is-primary is-small level-item"
|
|
||||||
disabled={true}
|
|
||||||
title="in development"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
Create Schema
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
className="button is-warning is-small level-item"
|
className="button is-warning is-small level-item"
|
||||||
disabled={true}
|
disabled={true}
|
||||||
|
@ -258,14 +242,6 @@ exports[`Details View when page with schema versions loaded when schema has vers
|
||||||
<div
|
<div
|
||||||
className="level-right"
|
className="level-right"
|
||||||
>
|
>
|
||||||
<button
|
|
||||||
className="button is-primary is-small level-item"
|
|
||||||
disabled={true}
|
|
||||||
title="in development"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
Create Schema
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
className="button is-warning is-small level-item"
|
className="button is-warning is-small level-item"
|
||||||
disabled={true}
|
disabled={true}
|
||||||
|
@ -397,14 +373,6 @@ exports[`Details View when page with schema versions loaded when versions are em
|
||||||
<div
|
<div
|
||||||
className="level-right"
|
className="level-right"
|
||||||
>
|
>
|
||||||
<button
|
|
||||||
className="button is-primary is-small level-item"
|
|
||||||
disabled={true}
|
|
||||||
title="in development"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
Create Schema
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
className="button is-warning is-small level-item"
|
className="button is-warning is-small level-item"
|
||||||
disabled={true}
|
disabled={true}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { SchemaSubject } from 'generated-sources';
|
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';
|
import ListItem from './ListItem';
|
||||||
|
|
||||||
export interface ListProps {
|
export interface ListProps {
|
||||||
|
@ -8,9 +10,24 @@ export interface ListProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const List: React.FC<ListProps> = ({ schemas }) => {
|
const List: React.FC<ListProps> = ({ schemas }) => {
|
||||||
|
const { clusterName } = useParams<{ clusterName: string }>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<Breadcrumb>Schema Registry</Breadcrumb>
|
<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">
|
<div className="box">
|
||||||
<table className="table is-striped is-fullwidth">
|
<table className="table is-striped is-fullwidth">
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -21,9 +38,15 @@ const List: React.FC<ListProps> = ({ schemas }) => {
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{schemas.map((subject) => (
|
{schemas.length > 0 ? (
|
||||||
<ListItem key={subject.id} subject={subject} />
|
schemas.map((subject) => (
|
||||||
))}
|
<ListItem key={subject.id} subject={subject} />
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={10}>No schemas found</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { RootState } from 'redux/interfaces';
|
import { RootState } from 'redux/interfaces';
|
||||||
import { withRouter } from 'react-router-dom';
|
|
||||||
import { getSchemaList } from 'redux/reducers/schemas/selectors';
|
import { getSchemaList } from 'redux/reducers/schemas/selectors';
|
||||||
import List from './List';
|
import List from './List';
|
||||||
|
|
||||||
|
@ -8,4 +7,4 @@ const mapStateToProps = (state: RootState) => ({
|
||||||
schemas: getSchemaList(state),
|
schemas: getSchemaList(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default withRouter(connect(mapStateToProps)(List));
|
export default connect(mapStateToProps)(List);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { mount, shallow } from 'enzyme';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { shallow } from 'enzyme';
|
import { StaticRouter } from 'react-router';
|
||||||
import configureStore from 'redux/store/configureStore';
|
import configureStore from 'redux/store/configureStore';
|
||||||
import ListContainer from '../ListContainer';
|
import ListContainer from '../ListContainer';
|
||||||
import List, { ListProps } from '../List';
|
import List, { ListProps } from '../List';
|
||||||
|
@ -22,35 +23,31 @@ describe('List', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('View', () => {
|
describe('View', () => {
|
||||||
|
const pathname = `/ui/clusters/clusterName/schemas`;
|
||||||
|
|
||||||
const setupWrapper = (props: Partial<ListProps> = {}) => (
|
const setupWrapper = (props: Partial<ListProps> = {}) => (
|
||||||
<List schemas={[]} {...props} />
|
<StaticRouter location={{ pathname }} context={{}}>
|
||||||
|
<List schemas={[]} {...props} />
|
||||||
|
</StaticRouter>
|
||||||
);
|
);
|
||||||
|
|
||||||
describe('without schemas', () => {
|
describe('without schemas', () => {
|
||||||
it('renders table heading without ListItem', () => {
|
it('renders table heading without ListItem', () => {
|
||||||
const wrapper = shallow(setupWrapper());
|
const wrapper = mount(setupWrapper());
|
||||||
expect(wrapper.exists('Breadcrumb')).toBeTruthy();
|
expect(wrapper.exists('Breadcrumb')).toBeTruthy();
|
||||||
expect(wrapper.exists('thead')).toBeTruthy();
|
expect(wrapper.exists('thead')).toBeTruthy();
|
||||||
expect(wrapper.exists('ListItem')).toBeFalsy();
|
expect(wrapper.exists('ListItem')).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('matches snapshot', () => {
|
|
||||||
expect(shallow(setupWrapper())).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with schemas', () => {
|
describe('with schemas', () => {
|
||||||
const wrapper = shallow(setupWrapper({ schemas }));
|
const wrapper = mount(setupWrapper({ schemas }));
|
||||||
|
|
||||||
it('renders table heading with ListItem', () => {
|
it('renders table heading with ListItem', () => {
|
||||||
expect(wrapper.exists('Breadcrumb')).toBeTruthy();
|
expect(wrapper.exists('Breadcrumb')).toBeTruthy();
|
||||||
expect(wrapper.exists('thead')).toBeTruthy();
|
expect(wrapper.exists('thead')).toBeTruthy();
|
||||||
expect(wrapper.find('ListItem').length).toEqual(3);
|
expect(wrapper.find('ListItem').length).toEqual(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('matches snapshot', () => {
|
|
||||||
expect(shallow(setupWrapper({ schemas }))).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>
|
|
||||||
`;
|
|
|
@ -12,7 +12,7 @@ export const schemas: SchemaSubject[] = [
|
||||||
{
|
{
|
||||||
subject: 'test2',
|
subject: 'test2',
|
||||||
version: '1',
|
version: '1',
|
||||||
id: 1,
|
id: 2,
|
||||||
schema:
|
schema:
|
||||||
'{"type":"record","name":"MyRecord2","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
|
'{"type":"record","name":"MyRecord2","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
|
||||||
compatibilityLevel: 'BACKWARD',
|
compatibilityLevel: 'BACKWARD',
|
||||||
|
@ -20,7 +20,7 @@ export const schemas: SchemaSubject[] = [
|
||||||
{
|
{
|
||||||
subject: 'test3',
|
subject: 'test3',
|
||||||
version: '1',
|
version: '1',
|
||||||
id: 1,
|
id: 12,
|
||||||
schema:
|
schema:
|
||||||
'{"type":"record","name":"MyRecord3","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
|
'{"type":"record","name":"MyRecord3","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
|
||||||
compatibilityLevel: 'BACKWARD',
|
compatibilityLevel: 'BACKWARD',
|
||||||
|
|
115
kafka-ui-react-app/src/components/Schemas/New/New.tsx
Normal file
115
kafka-ui-react-app/src/components/Schemas/New/New.tsx
Normal 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;
|
|
@ -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);
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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>
|
||||||
|
`;
|
|
@ -1,43 +1,49 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ClusterName } from 'redux/interfaces';
|
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 PageLoader from 'components/common/PageLoader/PageLoader';
|
||||||
import ListContainer from './List/ListContainer';
|
import ListContainer from './List/ListContainer';
|
||||||
import DetailsContainer from './Details/DetailsContainer';
|
import DetailsContainer from './Details/DetailsContainer';
|
||||||
|
import NewContainer from './New/NewContainer';
|
||||||
|
|
||||||
export interface SchemasProps {
|
export interface SchemasProps {
|
||||||
isFetched: boolean;
|
isFetching: boolean;
|
||||||
clusterName: ClusterName;
|
|
||||||
fetchSchemasByClusterName: (clusterName: ClusterName) => void;
|
fetchSchemasByClusterName: (clusterName: ClusterName) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Schemas: React.FC<SchemasProps> = ({
|
const Schemas: React.FC<SchemasProps> = ({
|
||||||
isFetched,
|
isFetching,
|
||||||
fetchSchemasByClusterName,
|
fetchSchemasByClusterName,
|
||||||
clusterName,
|
|
||||||
}) => {
|
}) => {
|
||||||
|
const { clusterName } = useParams<{ clusterName: string }>();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
fetchSchemasByClusterName(clusterName);
|
fetchSchemasByClusterName(clusterName);
|
||||||
}, [fetchSchemasByClusterName, clusterName]);
|
}, [fetchSchemasByClusterName, clusterName]);
|
||||||
|
|
||||||
if (isFetched) {
|
if (isFetching) {
|
||||||
return (
|
return <PageLoader />;
|
||||||
<Switch>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path="/ui/clusters/:clusterName/schemas"
|
|
||||||
component={ListContainer}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path="/ui/clusters/:clusterName/schemas/:subject/latest"
|
|
||||||
component={DetailsContainer}
|
|
||||||
/>
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
export default Schemas;
|
||||||
|
|
|
@ -1,32 +1,15 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { ClusterName, RootState } from 'redux/interfaces';
|
import { RootState } from 'redux/interfaces';
|
||||||
import { fetchSchemasByClusterName } from 'redux/actions';
|
import { fetchSchemasByClusterName } from 'redux/actions';
|
||||||
import { getIsSchemaListFetched } from 'redux/reducers/schemas/selectors';
|
import { getIsSchemaListFetching } from 'redux/reducers/schemas/selectors';
|
||||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
|
||||||
import Schemas from './Schemas';
|
import Schemas from './Schemas';
|
||||||
|
|
||||||
interface RouteProps {
|
const mapStateToProps = (state: RootState) => ({
|
||||||
clusterName: ClusterName;
|
isFetching: getIsSchemaListFetching(state),
|
||||||
}
|
|
||||||
|
|
||||||
type OwnProps = RouteComponentProps<RouteProps>;
|
|
||||||
|
|
||||||
const mapStateToProps = (
|
|
||||||
state: RootState,
|
|
||||||
{
|
|
||||||
match: {
|
|
||||||
params: { clusterName },
|
|
||||||
},
|
|
||||||
}: OwnProps
|
|
||||||
) => ({
|
|
||||||
isFetched: getIsSchemaListFetched(state),
|
|
||||||
clusterName,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
fetchSchemasByClusterName,
|
fetchSchemasByClusterName,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withRouter(
|
export default connect(mapStateToProps, mapDispatchToProps)(Schemas);
|
||||||
connect(mapStateToProps, mapDispatchToProps)(Schemas)
|
|
||||||
);
|
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { shallow } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import configureStore from 'redux/store/configureStore';
|
import configureStore from 'redux/store/configureStore';
|
||||||
import { StaticRouter } from 'react-router-dom';
|
import { StaticRouter } from 'react-router-dom';
|
||||||
import { match } from 'react-router';
|
|
||||||
import { ClusterName } from 'redux/interfaces';
|
|
||||||
import Schemas, { SchemasProps } from '../Schemas';
|
import Schemas, { SchemasProps } from '../Schemas';
|
||||||
import SchemasContainer from '../SchemasContainer';
|
import SchemasContainer from '../SchemasContainer';
|
||||||
|
|
||||||
|
@ -15,7 +13,7 @@ describe('Schemas', () => {
|
||||||
const store = configureStore();
|
const store = configureStore();
|
||||||
|
|
||||||
it('renders view', () => {
|
it('renders view', () => {
|
||||||
const component = shallow(
|
const component = mount(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<StaticRouter location={{ pathname }} context={{}}>
|
<StaticRouter location={{ pathname }} context={{}}>
|
||||||
<SchemasContainer />
|
<SchemasContainer />
|
||||||
|
@ -28,12 +26,13 @@ describe('Schemas', () => {
|
||||||
|
|
||||||
describe('View', () => {
|
describe('View', () => {
|
||||||
const setupWrapper = (props: Partial<SchemasProps> = {}) => (
|
const setupWrapper = (props: Partial<SchemasProps> = {}) => (
|
||||||
<Schemas
|
<StaticRouter location={{ pathname }} context={{}}>
|
||||||
isFetched
|
<Schemas
|
||||||
clusterName="Test"
|
isFetching
|
||||||
fetchSchemasByClusterName={jest.fn()}
|
fetchSchemasByClusterName={jest.fn()}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
</StaticRouter>
|
||||||
);
|
);
|
||||||
describe('Initial state', () => {
|
describe('Initial state', () => {
|
||||||
let useEffect: jest.SpyInstance<
|
let useEffect: jest.SpyInstance<
|
||||||
|
@ -43,7 +42,6 @@ describe('Schemas', () => {
|
||||||
deps?: React.DependencyList | undefined
|
deps?: React.DependencyList | undefined
|
||||||
]
|
]
|
||||||
>;
|
>;
|
||||||
let wrapper;
|
|
||||||
const mockedFn = jest.fn();
|
const mockedFn = jest.fn();
|
||||||
|
|
||||||
const mockedUseEffect = () => {
|
const mockedUseEffect = () => {
|
||||||
|
@ -53,33 +51,20 @@ describe('Schemas', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
useEffect = jest.spyOn(React, 'useEffect');
|
useEffect = jest.spyOn(React, 'useEffect');
|
||||||
mockedUseEffect();
|
mockedUseEffect();
|
||||||
|
|
||||||
wrapper = shallow(
|
|
||||||
setupWrapper({ fetchSchemasByClusterName: mockedFn })
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call fetchSchemasByClusterName every render', () => {
|
it('should call fetchSchemasByClusterName every render', () => {
|
||||||
|
mount(setupWrapper({ fetchSchemasByClusterName: mockedFn }));
|
||||||
expect(mockedFn).toHaveBeenCalled();
|
expect(mockedFn).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('matches snapshot', () => {
|
|
||||||
expect(
|
|
||||||
shallow(setupWrapper({ fetchSchemasByClusterName: mockedFn }))
|
|
||||||
).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when page is loading', () => {
|
describe('when page is loading', () => {
|
||||||
const wrapper = shallow(setupWrapper({ isFetched: false }));
|
const wrapper = mount(setupWrapper({ isFetching: true }));
|
||||||
|
|
||||||
it('renders PageLoader', () => {
|
it('renders PageLoader', () => {
|
||||||
expect(wrapper.exists('PageLoader')).toBeTruthy();
|
expect(wrapper.exists('PageLoader')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('matches snapshot', () => {
|
|
||||||
expect(shallow(setupWrapper({ isFetched: false }))).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 />`;
|
|
|
@ -9,6 +9,7 @@ export const BASE_PARAMS: ConfigurationParameters = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TOPIC_NAME_VALIDATION_PATTERN = RegExp(/^[.,A-Za-z0-9_-]+$/);
|
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_WEEK = 604_800_000;
|
||||||
export const MILLISECONDS_IN_DAY = 86_400_000;
|
export const MILLISECONDS_IN_DAY = 86_400_000;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ClusterName, TopicName } from 'redux/interfaces';
|
import { ClusterName, SchemaName, TopicName } from 'redux/interfaces';
|
||||||
|
|
||||||
const clusterPath = (clusterName: ClusterName) => `/ui/clusters/${clusterName}`;
|
const clusterPath = (clusterName: ClusterName) => `/ui/clusters/${clusterName}`;
|
||||||
|
|
||||||
|
@ -12,6 +12,8 @@ export const clusterConsumerGroupsPath = (clusterName: ClusterName) =>
|
||||||
`${clusterPath(clusterName)}/consumer-groups`;
|
`${clusterPath(clusterName)}/consumer-groups`;
|
||||||
export const clusterSchemasPath = (clusterName: ClusterName) =>
|
export const clusterSchemasPath = (clusterName: ClusterName) =>
|
||||||
`${clusterPath(clusterName)}/schemas`;
|
`${clusterPath(clusterName)}/schemas`;
|
||||||
|
export const clusterSchemaNewPath = (clusterName: ClusterName) =>
|
||||||
|
`${clusterPath(clusterName)}/schemas/new`;
|
||||||
|
|
||||||
export const clusterTopicPath = (
|
export const clusterTopicPath = (
|
||||||
clusterName: ClusterName,
|
clusterName: ClusterName,
|
||||||
|
@ -30,3 +32,8 @@ export const clusterTopicsTopicEditPath = (
|
||||||
clusterName: ClusterName,
|
clusterName: ClusterName,
|
||||||
topicName: TopicName
|
topicName: TopicName
|
||||||
) => `${clusterTopicsPath(clusterName)}/${topicName}/edit`;
|
) => `${clusterTopicsPath(clusterName)}/${topicName}/edit`;
|
||||||
|
|
||||||
|
export const clusterSchemaPath = (
|
||||||
|
clusterName: ClusterName,
|
||||||
|
subject: SchemaName
|
||||||
|
) => `${clusterSchemasPath(clusterName)}/${subject}/latest`;
|
||||||
|
|
|
@ -6,13 +6,13 @@ import * as actions from '../actions';
|
||||||
|
|
||||||
describe('Actions', () => {
|
describe('Actions', () => {
|
||||||
describe('fetchClusterStatsAction', () => {
|
describe('fetchClusterStatsAction', () => {
|
||||||
it('creates an REQUEST action', () => {
|
it('creates a REQUEST action', () => {
|
||||||
expect(actions.fetchClusterStatsAction.request()).toEqual({
|
expect(actions.fetchClusterStatsAction.request()).toEqual({
|
||||||
type: 'GET_CLUSTER_STATUS__REQUEST',
|
type: 'GET_CLUSTER_STATUS__REQUEST',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates an SUCCESS action', () => {
|
it('creates a SUCCESS action', () => {
|
||||||
expect(
|
expect(
|
||||||
actions.fetchClusterStatsAction.success({ brokerCount: 1 })
|
actions.fetchClusterStatsAction.success({ brokerCount: 1 })
|
||||||
).toEqual({
|
).toEqual({
|
||||||
|
@ -23,7 +23,7 @@ describe('Actions', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates an FAILURE action', () => {
|
it('creates a FAILURE action', () => {
|
||||||
expect(actions.fetchClusterStatsAction.failure()).toEqual({
|
expect(actions.fetchClusterStatsAction.failure()).toEqual({
|
||||||
type: 'GET_CLUSTER_STATUS__FAILURE',
|
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',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ClusterStats } from 'generated-sources';
|
import { ClusterStats, NewSchemaSubject } from 'generated-sources';
|
||||||
|
|
||||||
export const clusterStats: ClusterStats = {
|
export const clusterStats: ClusterStats = {
|
||||||
brokerCount: 1,
|
brokerCount: 1,
|
||||||
|
@ -11,3 +11,8 @@ export const clusterStats: ClusterStats = {
|
||||||
underReplicatedPartitionCount: 0,
|
underReplicatedPartitionCount: 0,
|
||||||
diskUsage: [{ brokerId: 1, segmentSize: 6538, segmentCount: 6 }],
|
diskUsage: [{ brokerId: 1, segmentSize: 6538, segmentCount: 6 }],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const schemaPayload: NewSchemaSubject = {
|
||||||
|
schema:
|
||||||
|
'{"type":"record","name":"MyRecord1","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
|
||||||
|
};
|
||||||
|
|
|
@ -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();
|
||||||
|
// });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -109,3 +109,9 @@ export const fetchSchemaVersionsAction = createAsyncAction(
|
||||||
'GET_SCHEMA_VERSIONS__SUCCESS',
|
'GET_SCHEMA_VERSIONS__SUCCESS',
|
||||||
'GET_SCHEMA_VERSIONS__FAILURE'
|
'GET_SCHEMA_VERSIONS__FAILURE'
|
||||||
)<undefined, SchemaSubject[], undefined>();
|
)<undefined, SchemaSubject[], undefined>();
|
||||||
|
|
||||||
|
export const createSchemaAction = createAsyncAction(
|
||||||
|
'POST_SCHEMA__REQUEST',
|
||||||
|
'POST_SCHEMA__SUCCESS',
|
||||||
|
'POST_SCHEMA__FAILURE'
|
||||||
|
)<undefined, SchemaSubject, undefined>();
|
||||||
|
|
|
@ -5,6 +5,8 @@ import {
|
||||||
Topic,
|
Topic,
|
||||||
TopicFormData,
|
TopicFormData,
|
||||||
TopicConfig,
|
TopicConfig,
|
||||||
|
NewSchemaSubject,
|
||||||
|
SchemaSubject,
|
||||||
} from 'generated-sources';
|
} from 'generated-sources';
|
||||||
import {
|
import {
|
||||||
ConsumerGroupID,
|
ConsumerGroupID,
|
||||||
|
@ -280,3 +282,22 @@ export const fetchSchemaVersions = (
|
||||||
dispatch(actions.fetchSchemaVersionsAction.failure());
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { SchemaSubject } from 'generated-sources';
|
import { NewSchemaSubject, SchemaSubject } from 'generated-sources';
|
||||||
|
|
||||||
export type SchemaName = string;
|
export type SchemaName = string;
|
||||||
|
|
||||||
|
@ -7,3 +7,7 @@ export interface SchemasState {
|
||||||
allNames: SchemaName[];
|
allNames: SchemaName[];
|
||||||
currentSchemaVersions: SchemaSubject[];
|
currentSchemaVersions: SchemaSubject[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NewSchemaSubjectRaw extends NewSchemaSubject {
|
||||||
|
subject: string;
|
||||||
|
}
|
||||||
|
|
|
@ -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 [],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
|
@ -52,3 +52,47 @@ export const schemaVersionsPayload: SchemaSubject[] = [
|
||||||
compatibilityLevel: 'BACKWARD',
|
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',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
createSchemaAction,
|
||||||
fetchSchemasByClusterNameAction,
|
fetchSchemasByClusterNameAction,
|
||||||
fetchSchemaVersionsAction,
|
fetchSchemaVersionsAction,
|
||||||
} from 'redux/actions';
|
} from 'redux/actions';
|
||||||
|
@ -17,6 +18,9 @@ describe('Schemas reducer', () => {
|
||||||
expect(reducer(undefined, fetchSchemaVersionsAction.request())).toEqual(
|
expect(reducer(undefined, fetchSchemaVersionsAction.request())).toEqual(
|
||||||
initialState
|
initialState
|
||||||
);
|
);
|
||||||
|
expect(reducer(undefined, createSchemaAction.request())).toEqual(
|
||||||
|
initialState
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reacts on GET_CLUSTER_SCHEMAS__SUCCESS and returns payload', () => {
|
it('reacts on GET_CLUSTER_SCHEMAS__SUCCESS and returns payload', () => {
|
||||||
|
@ -36,4 +40,10 @@ describe('Schemas reducer', () => {
|
||||||
)
|
)
|
||||||
).toMatchSnapshot();
|
).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('reacts on POST_SCHEMA__SUCCESS and returns payload', () => {
|
||||||
|
expect(
|
||||||
|
reducer(undefined, createSchemaAction.success(schemaVersionsPayload[0]))
|
||||||
|
).toMatchSnapshot();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
import {
|
import {
|
||||||
|
createSchemaAction,
|
||||||
fetchSchemasByClusterNameAction,
|
fetchSchemasByClusterNameAction,
|
||||||
fetchSchemaVersionsAction,
|
fetchSchemaVersionsAction,
|
||||||
} from 'redux/actions';
|
} from 'redux/actions';
|
||||||
import configureStore from 'redux/store/configureStore';
|
import configureStore from 'redux/store/configureStore';
|
||||||
import * as selectors from '../selectors';
|
import * as selectors from '../selectors';
|
||||||
import { clusterSchemasPayload, schemaVersionsPayload } from './fixtures';
|
import {
|
||||||
|
clusterSchemasPayload,
|
||||||
|
clusterSchemasPayloadWithNewSchema,
|
||||||
|
newSchemaPayload,
|
||||||
|
schemaVersionsPayload,
|
||||||
|
} from './fixtures';
|
||||||
|
|
||||||
const store = configureStore();
|
const store = configureStore();
|
||||||
|
|
||||||
|
@ -13,6 +19,7 @@ describe('Schemas selectors', () => {
|
||||||
it('returns fetch status', () => {
|
it('returns fetch status', () => {
|
||||||
expect(selectors.getIsSchemaListFetched(store.getState())).toBeFalsy();
|
expect(selectors.getIsSchemaListFetched(store.getState())).toBeFalsy();
|
||||||
expect(selectors.getIsSchemaVersionFetched(store.getState())).toBeFalsy();
|
expect(selectors.getIsSchemaVersionFetched(store.getState())).toBeFalsy();
|
||||||
|
expect(selectors.getSchemaCreated(store.getState())).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns schema list', () => {
|
it('returns schema list', () => {
|
||||||
|
@ -34,6 +41,7 @@ describe('Schemas selectors', () => {
|
||||||
fetchSchemasByClusterNameAction.success(clusterSchemasPayload)
|
fetchSchemasByClusterNameAction.success(clusterSchemasPayload)
|
||||||
);
|
);
|
||||||
store.dispatch(fetchSchemaVersionsAction.success(schemaVersionsPayload));
|
store.dispatch(fetchSchemaVersionsAction.success(schemaVersionsPayload));
|
||||||
|
store.dispatch(createSchemaAction.success(newSchemaPayload));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns fetch status', () => {
|
it('returns fetch status', () => {
|
||||||
|
@ -41,11 +49,12 @@ describe('Schemas selectors', () => {
|
||||||
expect(
|
expect(
|
||||||
selectors.getIsSchemaVersionFetched(store.getState())
|
selectors.getIsSchemaVersionFetched(store.getState())
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
|
expect(selectors.getSchemaCreated(store.getState())).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns schema list', () => {
|
it('returns schema list', () => {
|
||||||
expect(selectors.getSchemaList(store.getState())).toEqual(
|
expect(selectors.getSchemaList(store.getState())).toEqual(
|
||||||
clusterSchemasPayload
|
clusterSchemasPayloadWithNewSchema
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -32,12 +32,26 @@ const updateSchemaList = (
|
||||||
}, initialMemo);
|
}, 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 => {
|
const reducer = (state = initialState, action: Action): SchemasState => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'GET_CLUSTER_SCHEMAS__SUCCESS':
|
case 'GET_CLUSTER_SCHEMAS__SUCCESS':
|
||||||
return updateSchemaList(state, action.payload);
|
return updateSchemaList(state, action.payload);
|
||||||
case 'GET_SCHEMA_VERSIONS__SUCCESS':
|
case 'GET_SCHEMA_VERSIONS__SUCCESS':
|
||||||
return { ...state, currentSchemaVersions: action.payload };
|
return { ...state, currentSchemaVersions: action.payload };
|
||||||
|
case 'POST_SCHEMA__SUCCESS':
|
||||||
|
return addToSchemaList(state, action.payload);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,16 +15,28 @@ const getSchemaVersionsFetchingStatus = createFetchingSelector(
|
||||||
'GET_SCHEMA_VERSIONS'
|
'GET_SCHEMA_VERSIONS'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getSchemaCreationStatus = createFetchingSelector('POST_SCHEMA');
|
||||||
|
|
||||||
export const getIsSchemaListFetched = createSelector(
|
export const getIsSchemaListFetched = createSelector(
|
||||||
getSchemaListFetchingStatus,
|
getSchemaListFetchingStatus,
|
||||||
(status) => status === 'fetched'
|
(status) => status === 'fetched'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const getIsSchemaListFetching = createSelector(
|
||||||
|
getSchemaListFetchingStatus,
|
||||||
|
(status) => status === 'fetching' || status === 'notFetched'
|
||||||
|
);
|
||||||
|
|
||||||
export const getIsSchemaVersionFetched = createSelector(
|
export const getIsSchemaVersionFetched = createSelector(
|
||||||
getSchemaVersionsFetchingStatus,
|
getSchemaVersionsFetchingStatus,
|
||||||
(status) => status === 'fetched'
|
(status) => status === 'fetched'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const getSchemaCreated = createSelector(
|
||||||
|
getSchemaCreationStatus,
|
||||||
|
(status) => status === 'fetched'
|
||||||
|
);
|
||||||
|
|
||||||
export const getSchemaList = createSelector(
|
export const getSchemaList = createSelector(
|
||||||
getIsSchemaListFetched,
|
getIsSchemaListFetched,
|
||||||
getAllNames,
|
getAllNames,
|
||||||
|
|
Loading…
Add table
Reference in a new issue