Schema Registry Views (#195)
* Schema Registry index page https://github.com/provectus/kafka-ui/pull/183 * Schema Registry show page https://github.com/provectus/kafka-ui/pull/196 * Specs https://github.com/provectus/kafka-ui/pull/208 * New JsonViewer common component
This commit is contained in:
parent
2c95928607
commit
3bc9447cc7
49 changed files with 2092 additions and 83 deletions
|
@ -30,6 +30,8 @@ services:
|
|||
environment:
|
||||
ZOOKEEPER_CLIENT_PORT: 2181
|
||||
ZOOKEEPER_TICK_TIME: 2000
|
||||
ports:
|
||||
- 2181:2181
|
||||
|
||||
kafka0:
|
||||
image: confluentinc/cp-kafka:5.1.0
|
||||
|
|
77
kafka-ui-react-app/package-lock.json
generated
77
kafka-ui-react-app/package-lock.json
generated
|
@ -2725,12 +2725,14 @@
|
|||
"@types/node": {
|
||||
"version": "12.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.2.tgz",
|
||||
"integrity": "sha512-djoyN0pvTje9Lpu25ZwZwlLQICPiuv42omiydLhl7om+og1RhQboGmar12KaKls8soTUQ893TuWCrlyt8B1pVg=="
|
||||
"integrity": "sha512-djoyN0pvTje9Lpu25ZwZwlLQICPiuv42omiydLhl7om+og1RhQboGmar12KaKls8soTUQ893TuWCrlyt8B1pVg==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/node-fetch": {
|
||||
"version": "2.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.8.tgz",
|
||||
"integrity": "sha512-fbjI6ja0N5ZA8TV53RUqzsKNkl9fv8Oj3T7zxW7FGv1GSH7gwJaNF8dzCjrqKaxKeUpTz4yT1DaJFq/omNpGfw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"form-data": "^3.0.0"
|
||||
|
@ -2740,6 +2742,7 @@
|
|||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
|
||||
"integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
|
@ -4214,7 +4217,8 @@
|
|||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
|
||||
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
|
||||
"dev": true
|
||||
},
|
||||
"at-least-node": {
|
||||
"version": "1.0.0",
|
||||
|
@ -5693,6 +5697,7 @@
|
|||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
|
@ -6643,7 +6648,8 @@
|
|||
"delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
|
||||
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
|
||||
"dev": true
|
||||
},
|
||||
"delegates": {
|
||||
"version": "1.0.0",
|
||||
|
@ -9332,19 +9338,6 @@
|
|||
"integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==",
|
||||
"dev": true
|
||||
},
|
||||
"history": {
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
|
||||
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.1.2",
|
||||
"loose-envify": "^1.2.0",
|
||||
"resolve-pathname": "^3.0.0",
|
||||
"tiny-invariant": "^1.0.2",
|
||||
"tiny-warning": "^1.0.0",
|
||||
"value-equal": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"hmac-drbg": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
|
||||
|
@ -12576,12 +12569,14 @@
|
|||
"mime-db": {
|
||||
"version": "1.45.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz",
|
||||
"integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w=="
|
||||
"integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==",
|
||||
"dev": true
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.28",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz",
|
||||
"integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"mime-db": "1.45.0"
|
||||
}
|
||||
|
@ -13570,9 +13565,9 @@
|
|||
}
|
||||
},
|
||||
"open": {
|
||||
"version": "7.4.1",
|
||||
"resolved": "https://registry.npmjs.org/open/-/open-7.4.1.tgz",
|
||||
"integrity": "sha512-Pxv+fKRsd/Ozflgn2Gjev1HZveJJeKR6hKKmdaImJMuEZ6htAvCTbcMABJo+qevlAelTLCrEK3YTKZ9fVTcSPw==",
|
||||
"version": "7.4.2",
|
||||
"resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz",
|
||||
"integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-docker": "^2.0.0",
|
||||
|
@ -15601,9 +15596,9 @@
|
|||
}
|
||||
},
|
||||
"react-dev-utils": {
|
||||
"version": "11.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-11.0.2.tgz",
|
||||
"integrity": "sha512-xG7GlMoYkrgc2M1kDCHKRywXMDbFnjOB+/VzpytQyYBusEzR8NlGTMmUbvN86k94yyKu5XReHB8eZC2JZrNchQ==",
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-11.0.3.tgz",
|
||||
"integrity": "sha512-4lEA5gF4OHrcJLMUV1t+4XbNDiJbsAWCH5Z2uqlTqW6dD7Cf5nEASkeXrCI/Mz83sI2o527oBIFKVMXtRf1Vtg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "7.10.4",
|
||||
|
@ -15619,7 +15614,7 @@
|
|||
"global-modules": "2.0.0",
|
||||
"globby": "11.0.1",
|
||||
"gzip-size": "5.1.1",
|
||||
"immer": "7.0.9",
|
||||
"immer": "8.0.1",
|
||||
"is-root": "2.1.0",
|
||||
"loader-utils": "2.0.0",
|
||||
"open": "^7.0.2",
|
||||
|
@ -15732,12 +15727,6 @@
|
|||
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
|
||||
"dev": true
|
||||
},
|
||||
"immer": {
|
||||
"version": "7.0.9",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-7.0.9.tgz",
|
||||
"integrity": "sha512-Vs/gxoM4DqNAYR7pugIxi0Xc8XAun/uy7AQu4fLLqaTBHxjOP9pJ266Q9MWA/ly4z6rAFZbvViOtihxUZ7O28A==",
|
||||
"dev": true
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
|
@ -15894,6 +15883,19 @@
|
|||
"tiny-warning": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"history": {
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
|
||||
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.1.2",
|
||||
"loose-envify": "^1.2.0",
|
||||
"resolve-pathname": "^3.0.0",
|
||||
"tiny-invariant": "^1.0.2",
|
||||
"tiny-warning": "^1.0.0",
|
||||
"value-equal": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
|
@ -15913,6 +15915,21 @@
|
|||
"react-router": "5.2.0",
|
||||
"tiny-invariant": "^1.0.2",
|
||||
"tiny-warning": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"history": {
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
|
||||
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.1.2",
|
||||
"loose-envify": "^1.2.0",
|
||||
"resolve-pathname": "^3.0.0",
|
||||
"tiny-invariant": "^1.0.2",
|
||||
"tiny-warning": "^1.0.0",
|
||||
"value-equal": "^1.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-scripts": {
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
"private": true,
|
||||
"dependencies": {
|
||||
"@hookform/error-message": "0.0.5",
|
||||
"@types/node-fetch": "^2.5.8",
|
||||
"bulma": "^0.9.2",
|
||||
"bulma-switch": "^2.0.0",
|
||||
"classnames": "^2.2.6",
|
||||
|
@ -22,6 +21,7 @@
|
|||
"react-json-tree": "^0.13.0",
|
||||
"react-multi-select-component": "^2.0.14",
|
||||
"react-redux": "^7.2.2",
|
||||
"react-router": "^5.2.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"redux": "^4.0.5",
|
||||
"redux-thunk": "^2.3.0",
|
||||
|
@ -77,6 +77,7 @@
|
|||
"@types/jest": "^26.0.20",
|
||||
"@types/lodash": "^4.14.165",
|
||||
"@types/node": "^12.20.2",
|
||||
"@types/node-fetch": "^2.5.8",
|
||||
"@types/react": "^17.0.2",
|
||||
"@types/react-datepicker": "^3.1.1",
|
||||
"@types/react-dom": "^17.0.1",
|
||||
|
|
|
@ -3,10 +3,11 @@ import { Switch, Route, Redirect } from 'react-router-dom';
|
|||
import './App.scss';
|
||||
import BrokersContainer from './Brokers/BrokersContainer';
|
||||
import TopicsContainer from './Topics/TopicsContainer';
|
||||
import NavConatiner from './Nav/NavConatiner';
|
||||
import NavContainer from './Nav/NavContainer';
|
||||
import PageLoader from './common/PageLoader/PageLoader';
|
||||
import Dashboard from './Dashboard/Dashboard';
|
||||
import ConsumersGroupsContainer from './ConsumerGroups/ConsumersGroupsContainer';
|
||||
import SchemasContainer from './Schemas/SchemasContainer';
|
||||
|
||||
interface AppProps {
|
||||
isClusterListFetched: boolean;
|
||||
|
@ -35,7 +36,7 @@ const App: React.FC<AppProps> = ({
|
|||
</div>
|
||||
</nav>
|
||||
<main className="Layout__container">
|
||||
<NavConatiner className="Layout__navbar" />
|
||||
<NavContainer className="Layout__navbar" />
|
||||
{isClusterListFetched ? (
|
||||
<Switch>
|
||||
<Route
|
||||
|
@ -55,6 +56,10 @@ const App: React.FC<AppProps> = ({
|
|||
path="/ui/clusters/:clusterName/consumer-groups"
|
||||
component={ConsumersGroupsContainer}
|
||||
/>
|
||||
<Route
|
||||
path="/ui/clusters/:clusterName/schemas"
|
||||
component={SchemasContainer}
|
||||
/>
|
||||
<Redirect
|
||||
from="/ui/clusters/:clusterName"
|
||||
to="/ui/clusters/:clusterName/brokers"
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
clusterBrokersPath,
|
||||
clusterTopicsPath,
|
||||
clusterConsumerGroupsPath,
|
||||
clusterSchemasPath,
|
||||
} from 'lib/paths';
|
||||
import { Cluster, ServerStatus } from 'generated-sources';
|
||||
|
||||
|
@ -85,6 +86,13 @@ const ClusterMenu: React.FC<Props> = ({ cluster }) => (
|
|||
>
|
||||
Consumers
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to={clusterSchemasPath(cluster.name)}
|
||||
activeClassName="is-active"
|
||||
title="Schema Registry"
|
||||
>
|
||||
Schema Registry
|
||||
</NavLink>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
110
kafka-ui-react-app/src/components/Schemas/Details/Details.tsx
Normal file
110
kafka-ui-react-app/src/components/Schemas/Details/Details.tsx
Normal file
|
@ -0,0 +1,110 @@
|
|||
import React from 'react';
|
||||
import { SchemaSubject } from 'generated-sources';
|
||||
import { ClusterName, SchemaName } from 'redux/interfaces';
|
||||
import { clusterSchemasPath } from 'lib/paths';
|
||||
import Breadcrumb from '../../common/Breadcrumb/Breadcrumb';
|
||||
import SchemaVersion from './SchemaVersion';
|
||||
import LatestVersionItem from './LatestVersionItem';
|
||||
import PageLoader from '../../common/PageLoader/PageLoader';
|
||||
|
||||
export interface DetailsProps {
|
||||
schema: SchemaSubject;
|
||||
clusterName: ClusterName;
|
||||
versions: SchemaSubject[];
|
||||
isFetched: boolean;
|
||||
fetchSchemaVersions: (
|
||||
clusterName: ClusterName,
|
||||
schemaName: SchemaName
|
||||
) => void;
|
||||
}
|
||||
|
||||
const Details: React.FC<DetailsProps> = ({
|
||||
schema,
|
||||
clusterName,
|
||||
fetchSchemaVersions,
|
||||
versions,
|
||||
isFetched,
|
||||
}) => {
|
||||
React.useEffect(() => {
|
||||
fetchSchemaVersions(clusterName, schema.subject as SchemaName);
|
||||
}, [fetchSchemaVersions, clusterName]);
|
||||
return (
|
||||
<div className="section">
|
||||
<div className="level">
|
||||
<Breadcrumb
|
||||
links={[
|
||||
{
|
||||
href: clusterSchemasPath(clusterName),
|
||||
label: 'Schema Registry',
|
||||
},
|
||||
]}
|
||||
>
|
||||
{schema.subject}
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
<div className="box">
|
||||
<div className="level">
|
||||
<div className="level-left">
|
||||
<div className="level-item">
|
||||
<div className="mr-1">
|
||||
<b>Latest Version</b>
|
||||
</div>
|
||||
<div className="tag is-info is-light" title="Version">
|
||||
#{schema.version}
|
||||
</div>
|
||||
</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"
|
||||
title="in development"
|
||||
disabled
|
||||
>
|
||||
Update Schema
|
||||
</button>
|
||||
<button
|
||||
className="button is-danger is-small level-item"
|
||||
type="button"
|
||||
title="in development"
|
||||
disabled
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<LatestVersionItem schema={schema} />
|
||||
</div>
|
||||
{isFetched ? (
|
||||
<div className="box">
|
||||
<table className="table is-striped is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Version</th>
|
||||
<th>ID</th>
|
||||
<th>Schema</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{versions.map((version) => (
|
||||
<SchemaVersion key={version.id} version={version} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<PageLoader />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Details;
|
|
@ -0,0 +1,39 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { ClusterName, RootState } from 'redux/interfaces';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
import {
|
||||
getIsSchemaVersionFetched,
|
||||
getSchema,
|
||||
getSortedSchemaVersions,
|
||||
} from 'redux/reducers/schemas/selectors';
|
||||
import { fetchSchemaVersions } from 'redux/actions';
|
||||
import Details from './Details';
|
||||
|
||||
interface RouteProps {
|
||||
clusterName: ClusterName;
|
||||
subject: string;
|
||||
}
|
||||
|
||||
type OwnProps = RouteComponentProps<RouteProps>;
|
||||
|
||||
const mapStateToProps = (
|
||||
state: RootState,
|
||||
{
|
||||
match: {
|
||||
params: { clusterName, subject },
|
||||
},
|
||||
}: OwnProps
|
||||
) => ({
|
||||
schema: getSchema(state, subject),
|
||||
versions: getSortedSchemaVersions(state),
|
||||
isFetched: getIsSchemaVersionFetched(state),
|
||||
clusterName,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchSchemaVersions,
|
||||
};
|
||||
|
||||
export default withRouter(
|
||||
connect(mapStateToProps, mapDispatchToProps)(Details)
|
||||
);
|
|
@ -0,0 +1,43 @@
|
|||
import React from 'react';
|
||||
import { SchemaSubject } from 'generated-sources';
|
||||
import JSONViewer from 'components/common/JSONViewer/JSONViewer';
|
||||
|
||||
interface LatestVersionProps {
|
||||
schema: SchemaSubject;
|
||||
}
|
||||
|
||||
const LatestVersionItem: React.FC<LatestVersionProps> = ({
|
||||
schema: { id, subject, schema, compatibilityLevel },
|
||||
}) => {
|
||||
return (
|
||||
<div className="tile is-ancestor mt-1">
|
||||
<div className="tile is-4 is-parent">
|
||||
<div className="tile is-child">
|
||||
<table className="table is-fullwidth">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>ID</td>
|
||||
<td>{id}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Subject</td>
|
||||
<td>{subject}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Compatibility</td>
|
||||
<td>{compatibilityLevel}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div className="tile is-parent">
|
||||
<div className="tile is-child box py-1">
|
||||
<JSONViewer data={JSON.parse(schema as string)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LatestVersionItem;
|
|
@ -0,0 +1,23 @@
|
|||
import React from 'react';
|
||||
import { SchemaSubject } from 'generated-sources';
|
||||
import JSONViewer from 'components/common/JSONViewer/JSONViewer';
|
||||
|
||||
interface SchemaVersionProps {
|
||||
version: SchemaSubject;
|
||||
}
|
||||
|
||||
const SchemaVersion: React.FC<SchemaVersionProps> = ({
|
||||
version: { version, id, schema },
|
||||
}) => {
|
||||
return (
|
||||
<tr>
|
||||
<td>{version}</td>
|
||||
<td>{id}</td>
|
||||
<td className="py-0">
|
||||
<JSONViewer data={JSON.parse(schema as string)} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
export default SchemaVersion;
|
|
@ -0,0 +1,108 @@
|
|||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { shallow } from 'enzyme';
|
||||
import configureStore from 'redux/store/configureStore';
|
||||
import DetailsContainer from '../DetailsContainer';
|
||||
import Details, { DetailsProps } from '../Details';
|
||||
import { schema, versions } from './fixtures';
|
||||
|
||||
describe('Details', () => {
|
||||
describe('Container', () => {
|
||||
const store = configureStore();
|
||||
|
||||
it('renders view', () => {
|
||||
const component = shallow(
|
||||
<Provider store={store}>
|
||||
<DetailsContainer />
|
||||
</Provider>
|
||||
);
|
||||
|
||||
expect(component.exists()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('View', () => {
|
||||
const setupWrapper = (props: Partial<DetailsProps> = {}) => (
|
||||
<Details
|
||||
schema={schema}
|
||||
clusterName="Test cluster"
|
||||
fetchSchemaVersions={jest.fn()}
|
||||
isFetched
|
||||
versions={[]}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
describe('Initial state', () => {
|
||||
let useEffect: jest.SpyInstance<
|
||||
void,
|
||||
[effect: React.EffectCallback, deps?: React.DependencyList | undefined]
|
||||
>;
|
||||
let wrapper;
|
||||
const mockedFn = jest.fn();
|
||||
|
||||
const mockedUseEffect = () => {
|
||||
useEffect.mockImplementationOnce(mockedFn);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
useEffect = jest.spyOn(React, 'useEffect');
|
||||
mockedUseEffect();
|
||||
|
||||
wrapper = shallow(setupWrapper({ fetchSchemaVersions: mockedFn }));
|
||||
});
|
||||
|
||||
it('should call fetchSchemaVersions every render', () => {
|
||||
expect(mockedFn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('matches snapshot', () => {
|
||||
expect(
|
||||
shallow(setupWrapper({ fetchSchemaVersions: mockedFn }))
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when page with schema versions is loading', () => {
|
||||
const wrapper = shallow(setupWrapper({ isFetched: false }));
|
||||
|
||||
it('renders PageLoader', () => {
|
||||
expect(wrapper.exists('PageLoader')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('matches snapshot', () => {
|
||||
expect(shallow(setupWrapper({ isFetched: false }))).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when page with schema versions loaded', () => {
|
||||
describe('when versions are empty', () => {
|
||||
it('renders table heading without SchemaVersion', () => {
|
||||
const wrapper = shallow(setupWrapper());
|
||||
expect(wrapper.exists('LatestVersionItem')).toBeTruthy();
|
||||
expect(wrapper.exists('button')).toBeTruthy();
|
||||
expect(wrapper.exists('thead')).toBeTruthy();
|
||||
expect(wrapper.exists('SchemaVersion')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('matches snapshot', () => {
|
||||
expect(shallow(setupWrapper())).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when schema has versions', () => {
|
||||
const wrapper = shallow(setupWrapper({ versions }));
|
||||
|
||||
it('renders table heading with SchemaVersion', () => {
|
||||
expect(wrapper.exists('LatestVersionItem')).toBeTruthy();
|
||||
expect(wrapper.exists('button')).toBeTruthy();
|
||||
expect(wrapper.exists('thead')).toBeTruthy();
|
||||
expect(wrapper.find('SchemaVersion').length).toEqual(2);
|
||||
});
|
||||
|
||||
it('matches snapshot', () => {
|
||||
expect(shallow(setupWrapper({ versions }))).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,18 @@
|
|||
import React from 'react';
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import { schema } from './fixtures';
|
||||
import LatestVersionItem from '../LatestVersionItem';
|
||||
|
||||
describe('LatestVersionItem', () => {
|
||||
it('renders latest version of schema', () => {
|
||||
const wrapper = mount(<LatestVersionItem schema={schema} />);
|
||||
|
||||
expect(wrapper.find('table').length).toEqual(1);
|
||||
expect(wrapper.find('td').at(1).text()).toEqual('1');
|
||||
expect(wrapper.exists('JSONViewer')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('matches snapshot', () => {
|
||||
expect(shallow(<LatestVersionItem schema={schema} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import SchemaVersion from '../SchemaVersion';
|
||||
import { versions } from './fixtures';
|
||||
|
||||
describe('SchemaVersion', () => {
|
||||
it('renders versions', () => {
|
||||
const wrapper = shallow(<SchemaVersion version={versions[0]} />);
|
||||
|
||||
expect(wrapper.find('td').length).toEqual(3);
|
||||
expect(wrapper.exists('JSONViewer')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('matches snapshot', () => {
|
||||
expect(shallow(<SchemaVersion version={versions[0]} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,461 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Details View Initial state matches snapshot 1`] = `
|
||||
<div
|
||||
className="section"
|
||||
>
|
||||
<div
|
||||
className="level"
|
||||
>
|
||||
<Breadcrumb
|
||||
links={
|
||||
Array [
|
||||
Object {
|
||||
"href": "/ui/clusters/Test cluster/schemas",
|
||||
"label": "Schema Registry",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
test
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
<div
|
||||
className="box"
|
||||
>
|
||||
<div
|
||||
className="level"
|
||||
>
|
||||
<div
|
||||
className="level-left"
|
||||
>
|
||||
<div
|
||||
className="level-item"
|
||||
>
|
||||
<div
|
||||
className="mr-1"
|
||||
>
|
||||
<b>
|
||||
Latest Version
|
||||
</b>
|
||||
</div>
|
||||
<div
|
||||
className="tag is-info is-light"
|
||||
title="Version"
|
||||
>
|
||||
#
|
||||
1
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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}
|
||||
title="in development"
|
||||
type="button"
|
||||
>
|
||||
Update Schema
|
||||
</button>
|
||||
<button
|
||||
className="button is-danger is-small level-item"
|
||||
disabled={true}
|
||||
title="in development"
|
||||
type="button"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<LatestVersionItem
|
||||
schema={
|
||||
Object {
|
||||
"compatibilityLevel": "BACKWARD",
|
||||
"id": 1,
|
||||
"schema": "{\\"type\\":\\"record\\",\\"name\\":\\"MyRecord1\\",\\"namespace\\":\\"com.mycompany\\",\\"fields\\":[{\\"name\\":\\"id\\",\\"type\\":\\"long\\"}]}",
|
||||
"subject": "test",
|
||||
"version": "1",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="box"
|
||||
>
|
||||
<table
|
||||
className="table is-striped is-fullwidth"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Version
|
||||
</th>
|
||||
<th>
|
||||
ID
|
||||
</th>
|
||||
<th>
|
||||
Schema
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody />
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Details View when page with schema versions is loading matches snapshot 1`] = `
|
||||
<div
|
||||
className="section"
|
||||
>
|
||||
<div
|
||||
className="level"
|
||||
>
|
||||
<Breadcrumb
|
||||
links={
|
||||
Array [
|
||||
Object {
|
||||
"href": "/ui/clusters/Test cluster/schemas",
|
||||
"label": "Schema Registry",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
test
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
<div
|
||||
className="box"
|
||||
>
|
||||
<div
|
||||
className="level"
|
||||
>
|
||||
<div
|
||||
className="level-left"
|
||||
>
|
||||
<div
|
||||
className="level-item"
|
||||
>
|
||||
<div
|
||||
className="mr-1"
|
||||
>
|
||||
<b>
|
||||
Latest Version
|
||||
</b>
|
||||
</div>
|
||||
<div
|
||||
className="tag is-info is-light"
|
||||
title="Version"
|
||||
>
|
||||
#
|
||||
1
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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}
|
||||
title="in development"
|
||||
type="button"
|
||||
>
|
||||
Update Schema
|
||||
</button>
|
||||
<button
|
||||
className="button is-danger is-small level-item"
|
||||
disabled={true}
|
||||
title="in development"
|
||||
type="button"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<LatestVersionItem
|
||||
schema={
|
||||
Object {
|
||||
"compatibilityLevel": "BACKWARD",
|
||||
"id": 1,
|
||||
"schema": "{\\"type\\":\\"record\\",\\"name\\":\\"MyRecord1\\",\\"namespace\\":\\"com.mycompany\\",\\"fields\\":[{\\"name\\":\\"id\\",\\"type\\":\\"long\\"}]}",
|
||||
"subject": "test",
|
||||
"version": "1",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<PageLoader />
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Details View when page with schema versions loaded when schema has versions matches snapshot 1`] = `
|
||||
<div
|
||||
className="section"
|
||||
>
|
||||
<div
|
||||
className="level"
|
||||
>
|
||||
<Breadcrumb
|
||||
links={
|
||||
Array [
|
||||
Object {
|
||||
"href": "/ui/clusters/Test cluster/schemas",
|
||||
"label": "Schema Registry",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
test
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
<div
|
||||
className="box"
|
||||
>
|
||||
<div
|
||||
className="level"
|
||||
>
|
||||
<div
|
||||
className="level-left"
|
||||
>
|
||||
<div
|
||||
className="level-item"
|
||||
>
|
||||
<div
|
||||
className="mr-1"
|
||||
>
|
||||
<b>
|
||||
Latest Version
|
||||
</b>
|
||||
</div>
|
||||
<div
|
||||
className="tag is-info is-light"
|
||||
title="Version"
|
||||
>
|
||||
#
|
||||
1
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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}
|
||||
title="in development"
|
||||
type="button"
|
||||
>
|
||||
Update Schema
|
||||
</button>
|
||||
<button
|
||||
className="button is-danger is-small level-item"
|
||||
disabled={true}
|
||||
title="in development"
|
||||
type="button"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<LatestVersionItem
|
||||
schema={
|
||||
Object {
|
||||
"compatibilityLevel": "BACKWARD",
|
||||
"id": 1,
|
||||
"schema": "{\\"type\\":\\"record\\",\\"name\\":\\"MyRecord1\\",\\"namespace\\":\\"com.mycompany\\",\\"fields\\":[{\\"name\\":\\"id\\",\\"type\\":\\"long\\"}]}",
|
||||
"subject": "test",
|
||||
"version": "1",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="box"
|
||||
>
|
||||
<table
|
||||
className="table is-striped is-fullwidth"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Version
|
||||
</th>
|
||||
<th>
|
||||
ID
|
||||
</th>
|
||||
<th>
|
||||
Schema
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<SchemaVersion
|
||||
key="1"
|
||||
version={
|
||||
Object {
|
||||
"compatibilityLevel": "BACKWARD",
|
||||
"id": 1,
|
||||
"schema": "{\\"type\\":\\"record\\",\\"name\\":\\"MyRecord1\\",\\"namespace\\":\\"com.mycompany\\",\\"fields\\":[{\\"name\\":\\"id\\",\\"type\\":\\"long\\"}]}",
|
||||
"subject": "test",
|
||||
"version": "1",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<SchemaVersion
|
||||
key="2"
|
||||
version={
|
||||
Object {
|
||||
"compatibilityLevel": "BACKWARD",
|
||||
"id": 2,
|
||||
"schema": "{\\"type\\":\\"record\\",\\"name\\":\\"MyRecord2\\",\\"namespace\\":\\"com.mycompany\\",\\"fields\\":[{\\"name\\":\\"id\\",\\"type\\":\\"long\\"}]}",
|
||||
"subject": "test",
|
||||
"version": "2",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`Details View when page with schema versions loaded when versions are empty matches snapshot 1`] = `
|
||||
<div
|
||||
className="section"
|
||||
>
|
||||
<div
|
||||
className="level"
|
||||
>
|
||||
<Breadcrumb
|
||||
links={
|
||||
Array [
|
||||
Object {
|
||||
"href": "/ui/clusters/Test cluster/schemas",
|
||||
"label": "Schema Registry",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
test
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
<div
|
||||
className="box"
|
||||
>
|
||||
<div
|
||||
className="level"
|
||||
>
|
||||
<div
|
||||
className="level-left"
|
||||
>
|
||||
<div
|
||||
className="level-item"
|
||||
>
|
||||
<div
|
||||
className="mr-1"
|
||||
>
|
||||
<b>
|
||||
Latest Version
|
||||
</b>
|
||||
</div>
|
||||
<div
|
||||
className="tag is-info is-light"
|
||||
title="Version"
|
||||
>
|
||||
#
|
||||
1
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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}
|
||||
title="in development"
|
||||
type="button"
|
||||
>
|
||||
Update Schema
|
||||
</button>
|
||||
<button
|
||||
className="button is-danger is-small level-item"
|
||||
disabled={true}
|
||||
title="in development"
|
||||
type="button"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<LatestVersionItem
|
||||
schema={
|
||||
Object {
|
||||
"compatibilityLevel": "BACKWARD",
|
||||
"id": 1,
|
||||
"schema": "{\\"type\\":\\"record\\",\\"name\\":\\"MyRecord1\\",\\"namespace\\":\\"com.mycompany\\",\\"fields\\":[{\\"name\\":\\"id\\",\\"type\\":\\"long\\"}]}",
|
||||
"subject": "test",
|
||||
"version": "1",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="box"
|
||||
>
|
||||
<table
|
||||
className="table is-striped is-fullwidth"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Version
|
||||
</th>
|
||||
<th>
|
||||
ID
|
||||
</th>
|
||||
<th>
|
||||
Schema
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody />
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,69 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`LatestVersionItem matches snapshot 1`] = `
|
||||
<div
|
||||
className="tile is-ancestor mt-1"
|
||||
>
|
||||
<div
|
||||
className="tile is-4 is-parent"
|
||||
>
|
||||
<div
|
||||
className="tile is-child"
|
||||
>
|
||||
<table
|
||||
className="table is-fullwidth"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
ID
|
||||
</td>
|
||||
<td>
|
||||
1
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Subject
|
||||
</td>
|
||||
<td>
|
||||
test
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Compatibility
|
||||
</td>
|
||||
<td>
|
||||
BACKWARD
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="tile is-parent"
|
||||
>
|
||||
<div
|
||||
className="tile is-child box py-1"
|
||||
>
|
||||
<JSONViewer
|
||||
data={
|
||||
Object {
|
||||
"fields": Array [
|
||||
Object {
|
||||
"name": "id",
|
||||
"type": "long",
|
||||
},
|
||||
],
|
||||
"name": "MyRecord1",
|
||||
"namespace": "com.mycompany",
|
||||
"type": "record",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,31 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SchemaVersion matches snapshot 1`] = `
|
||||
<tr>
|
||||
<td>
|
||||
1
|
||||
</td>
|
||||
<td>
|
||||
1
|
||||
</td>
|
||||
<td
|
||||
className="py-0"
|
||||
>
|
||||
<JSONViewer
|
||||
data={
|
||||
Object {
|
||||
"fields": Array [
|
||||
Object {
|
||||
"name": "id",
|
||||
"type": "long",
|
||||
},
|
||||
],
|
||||
"name": "MyRecord1",
|
||||
"namespace": "com.mycompany",
|
||||
"type": "record",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
|
@ -0,0 +1,29 @@
|
|||
import { SchemaSubject } from 'generated-sources';
|
||||
|
||||
export const schema: SchemaSubject = {
|
||||
subject: 'test',
|
||||
version: '1',
|
||||
id: 1,
|
||||
schema:
|
||||
'{"type":"record","name":"MyRecord1","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
|
||||
compatibilityLevel: 'BACKWARD',
|
||||
};
|
||||
|
||||
export const versions: SchemaSubject[] = [
|
||||
{
|
||||
subject: 'test',
|
||||
version: '1',
|
||||
id: 1,
|
||||
schema:
|
||||
'{"type":"record","name":"MyRecord1","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',
|
||||
},
|
||||
];
|
34
kafka-ui-react-app/src/components/Schemas/List/List.tsx
Normal file
34
kafka-ui-react-app/src/components/Schemas/List/List.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import React from 'react';
|
||||
import { SchemaSubject } from 'generated-sources';
|
||||
import Breadcrumb from '../../common/Breadcrumb/Breadcrumb';
|
||||
import ListItem from './ListItem';
|
||||
|
||||
export interface ListProps {
|
||||
schemas: SchemaSubject[];
|
||||
}
|
||||
|
||||
const List: React.FC<ListProps> = ({ schemas }) => {
|
||||
return (
|
||||
<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>
|
||||
{schemas.map((subject) => (
|
||||
<ListItem key={subject.id} subject={subject} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default List;
|
|
@ -0,0 +1,11 @@
|
|||
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';
|
||||
|
||||
const mapStateToProps = (state: RootState) => ({
|
||||
schemas: getSchemaList(state),
|
||||
});
|
||||
|
||||
export default withRouter(connect(mapStateToProps)(List));
|
32
kafka-ui-react-app/src/components/Schemas/List/ListItem.tsx
Normal file
32
kafka-ui-react-app/src/components/Schemas/List/ListItem.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import React from 'react';
|
||||
import { SchemaSubject } from 'generated-sources';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
export interface ListItemProps {
|
||||
subject: SchemaSubject;
|
||||
}
|
||||
|
||||
const ListItem: React.FC<ListItemProps> = ({
|
||||
subject: { subject, version, compatibilityLevel },
|
||||
}) => {
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
<NavLink
|
||||
exact
|
||||
to={`schemas/${subject}/latest`}
|
||||
activeClassName="is-active"
|
||||
className="title is-6"
|
||||
>
|
||||
{subject}
|
||||
</NavLink>
|
||||
</td>
|
||||
<td>{version}</td>
|
||||
<td>
|
||||
<span className="tag is-link">{compatibilityLevel}</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListItem;
|
|
@ -0,0 +1,56 @@
|
|||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { shallow } from 'enzyme';
|
||||
import configureStore from 'redux/store/configureStore';
|
||||
import ListContainer from '../ListContainer';
|
||||
import List, { ListProps } from '../List';
|
||||
import { schemas } from './fixtures';
|
||||
|
||||
describe('List', () => {
|
||||
describe('Container', () => {
|
||||
const store = configureStore();
|
||||
|
||||
it('renders view', () => {
|
||||
const component = shallow(
|
||||
<Provider store={store}>
|
||||
<ListContainer />
|
||||
</Provider>
|
||||
);
|
||||
|
||||
expect(component.exists()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('View', () => {
|
||||
const setupWrapper = (props: Partial<ListProps> = {}) => (
|
||||
<List schemas={[]} {...props} />
|
||||
);
|
||||
|
||||
describe('without schemas', () => {
|
||||
it('renders table heading without ListItem', () => {
|
||||
const wrapper = shallow(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 }));
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,26 @@
|
|||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { schemas } from './fixtures';
|
||||
import ListItem from '../ListItem';
|
||||
|
||||
describe('ListItem', () => {
|
||||
const wrapper = mount(
|
||||
<Router>
|
||||
<table>
|
||||
<tbody>
|
||||
<ListItem subject={schemas[0]} />
|
||||
</tbody>
|
||||
</table>
|
||||
</Router>
|
||||
);
|
||||
|
||||
it('renders schemas', () => {
|
||||
expect(wrapper.find('NavLink').length).toEqual(1);
|
||||
expect(wrapper.find('td').length).toEqual(3);
|
||||
});
|
||||
|
||||
it('matches snapshot', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,102 @@
|
|||
// 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>
|
||||
`;
|
|
@ -0,0 +1,94 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ListItem matches snapshot 1`] = `
|
||||
<BrowserRouter>
|
||||
<Router
|
||||
history={
|
||||
Object {
|
||||
"action": "POP",
|
||||
"block": [Function],
|
||||
"createHref": [Function],
|
||||
"go": [Function],
|
||||
"goBack": [Function],
|
||||
"goForward": [Function],
|
||||
"length": 1,
|
||||
"listen": [Function],
|
||||
"location": Object {
|
||||
"hash": "",
|
||||
"pathname": "/",
|
||||
"search": "",
|
||||
"state": undefined,
|
||||
},
|
||||
"push": [Function],
|
||||
"replace": [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
<table>
|
||||
<tbody>
|
||||
<ListItem
|
||||
subject={
|
||||
Object {
|
||||
"compatibilityLevel": "BACKWARD",
|
||||
"id": 1,
|
||||
"schema": "{\\"type\\":\\"record\\",\\"name\\":\\"MyRecord1\\",\\"namespace\\":\\"com.mycompany\\",\\"fields\\":[{\\"name\\":\\"id\\",\\"type\\":\\"long\\"}]}",
|
||||
"subject": "test",
|
||||
"version": "1",
|
||||
}
|
||||
}
|
||||
>
|
||||
<tr>
|
||||
<td>
|
||||
<NavLink
|
||||
activeClassName="is-active"
|
||||
className="title is-6"
|
||||
exact={true}
|
||||
to="schemas/test/latest"
|
||||
>
|
||||
<Link
|
||||
aria-current={null}
|
||||
className="title is-6"
|
||||
to={
|
||||
Object {
|
||||
"hash": "",
|
||||
"pathname": "/schemas/test/latest",
|
||||
"search": "",
|
||||
"state": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<LinkAnchor
|
||||
aria-current={null}
|
||||
className="title is-6"
|
||||
href="/schemas/test/latest"
|
||||
navigate={[Function]}
|
||||
>
|
||||
<a
|
||||
aria-current={null}
|
||||
className="title is-6"
|
||||
href="/schemas/test/latest"
|
||||
onClick={[Function]}
|
||||
>
|
||||
test
|
||||
</a>
|
||||
</LinkAnchor>
|
||||
</Link>
|
||||
</NavLink>
|
||||
</td>
|
||||
<td>
|
||||
1
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
className="tag is-link"
|
||||
>
|
||||
BACKWARD
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</ListItem>
|
||||
</tbody>
|
||||
</table>
|
||||
</Router>
|
||||
</BrowserRouter>
|
||||
`;
|
|
@ -0,0 +1,28 @@
|
|||
import { SchemaSubject } from 'generated-sources';
|
||||
|
||||
export const schemas: SchemaSubject[] = [
|
||||
{
|
||||
subject: 'test',
|
||||
version: '1',
|
||||
id: 1,
|
||||
schema:
|
||||
'{"type":"record","name":"MyRecord1","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
|
||||
compatibilityLevel: 'BACKWARD',
|
||||
},
|
||||
{
|
||||
subject: 'test2',
|
||||
version: '1',
|
||||
id: 1,
|
||||
schema:
|
||||
'{"type":"record","name":"MyRecord2","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
|
||||
compatibilityLevel: 'BACKWARD',
|
||||
},
|
||||
{
|
||||
subject: 'test3',
|
||||
version: '1',
|
||||
id: 1,
|
||||
schema:
|
||||
'{"type":"record","name":"MyRecord3","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
|
||||
compatibilityLevel: 'BACKWARD',
|
||||
},
|
||||
];
|
43
kafka-ui-react-app/src/components/Schemas/Schemas.tsx
Normal file
43
kafka-ui-react-app/src/components/Schemas/Schemas.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import React from 'react';
|
||||
import { ClusterName } from 'redux/interfaces';
|
||||
import { Switch, Route } from 'react-router-dom';
|
||||
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||
import ListContainer from './List/ListContainer';
|
||||
import DetailsContainer from './Details/DetailsContainer';
|
||||
|
||||
export interface SchemasProps {
|
||||
isFetched: boolean;
|
||||
clusterName: ClusterName;
|
||||
fetchSchemasByClusterName: (clusterName: ClusterName) => void;
|
||||
}
|
||||
|
||||
const Schemas: React.FC<SchemasProps> = ({
|
||||
isFetched,
|
||||
fetchSchemasByClusterName,
|
||||
clusterName,
|
||||
}) => {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
return <PageLoader />;
|
||||
};
|
||||
|
||||
export default Schemas;
|
|
@ -0,0 +1,32 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { ClusterName, RootState } from 'redux/interfaces';
|
||||
import { fetchSchemasByClusterName } from 'redux/actions';
|
||||
import { getIsSchemaListFetched } from 'redux/reducers/schemas/selectors';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
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 mapDispatchToProps = {
|
||||
fetchSchemasByClusterName,
|
||||
};
|
||||
|
||||
export default withRouter(
|
||||
connect(mapStateToProps, mapDispatchToProps)(Schemas)
|
||||
);
|
|
@ -0,0 +1,86 @@
|
|||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { shallow } 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';
|
||||
|
||||
describe('Schemas', () => {
|
||||
const pathname = `/ui/clusters/clusterName/schemas`;
|
||||
|
||||
describe('Container', () => {
|
||||
const store = configureStore();
|
||||
|
||||
it('renders view', () => {
|
||||
const component = shallow(
|
||||
<Provider store={store}>
|
||||
<StaticRouter location={{ pathname }} context={{}}>
|
||||
<SchemasContainer />
|
||||
</StaticRouter>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
expect(component.exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('View', () => {
|
||||
const setupWrapper = (props: Partial<SchemasProps> = {}) => (
|
||||
<Schemas
|
||||
isFetched
|
||||
clusterName="Test"
|
||||
fetchSchemasByClusterName={jest.fn()}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
describe('Initial state', () => {
|
||||
let useEffect: jest.SpyInstance<
|
||||
void,
|
||||
[
|
||||
effect: React.EffectCallback,
|
||||
deps?: React.DependencyList | undefined
|
||||
]
|
||||
>;
|
||||
let wrapper;
|
||||
const mockedFn = jest.fn();
|
||||
|
||||
const mockedUseEffect = () => {
|
||||
useEffect.mockImplementationOnce(mockedFn);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
useEffect = jest.spyOn(React, 'useEffect');
|
||||
mockedUseEffect();
|
||||
|
||||
wrapper = shallow(
|
||||
setupWrapper({ fetchSchemasByClusterName: mockedFn })
|
||||
);
|
||||
});
|
||||
|
||||
it('should call fetchSchemasByClusterName every render', () => {
|
||||
expect(mockedFn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('matches snapshot', () => {
|
||||
expect(
|
||||
shallow(setupWrapper({ fetchSchemasByClusterName: mockedFn }))
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when page is loading', () => {
|
||||
const wrapper = shallow(setupWrapper({ isFetched: false }));
|
||||
|
||||
it('renders PageLoader', () => {
|
||||
expect(wrapper.exists('PageLoader')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('matches snapshot', () => {
|
||||
expect(shallow(setupWrapper({ isFetched: false }))).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,18 @@
|
|||
// 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 />`;
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import JSONTree from 'react-json-tree';
|
||||
import { TopicMessage } from 'generated-sources';
|
||||
import JSONViewer from 'components/common/JSONViewer/JSONViewer';
|
||||
|
||||
export interface MessageItemProp {
|
||||
partition: TopicMessage['partition'];
|
||||
|
@ -21,28 +21,7 @@ const MessageItem: React.FC<MessageItemProp> = ({
|
|||
<td style={{ width: 150 }}>{offset}</td>
|
||||
<td style={{ width: 100 }}>{partition}</td>
|
||||
<td style={{ wordBreak: 'break-word' }}>
|
||||
{content && (
|
||||
<JSONTree
|
||||
data={content}
|
||||
hideRoot
|
||||
invertTheme={false}
|
||||
theme={{
|
||||
tree: ({ style }) => ({
|
||||
style: {
|
||||
...style,
|
||||
backgroundColor: undefined,
|
||||
marginLeft: 0,
|
||||
marginTop: 0,
|
||||
},
|
||||
}),
|
||||
value: ({ style }) => ({
|
||||
style: { ...style, marginLeft: 0 },
|
||||
}),
|
||||
base0D: '#3273dc',
|
||||
base0B: '#363636',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{content && <JSONViewer data={content as { [key: string]: string }} />}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
|
|
@ -32,7 +32,7 @@ const MessagesTable: React.FC<MessagesTableProp> = ({ messages, onNext }) => {
|
|||
partition={partition}
|
||||
offset={offset}
|
||||
timestamp={timestamp}
|
||||
content={content as Record<string, unknown>}
|
||||
content={content as { [key: string]: string }}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
|
|
@ -14,7 +14,7 @@ describe('MessageItem', () => {
|
|||
|
||||
expect(wrapper.find('tr').length).toEqual(1);
|
||||
expect(wrapper.find('td').length).toEqual(4);
|
||||
expect(wrapper.find('JSONTree').length).toEqual(1);
|
||||
expect(wrapper.find('JSONViewer').length).toEqual(1);
|
||||
});
|
||||
|
||||
it('matches snapshot', () => {
|
||||
|
@ -28,7 +28,7 @@ describe('MessageItem', () => {
|
|||
|
||||
expect(wrapper.find('tr').length).toEqual(1);
|
||||
expect(wrapper.find('td').length).toEqual(4);
|
||||
expect(wrapper.find('JSONTree').length).toEqual(0);
|
||||
expect(wrapper.find('JSONViewer').length).toEqual(0);
|
||||
});
|
||||
|
||||
it('matches snapshot', () => {
|
||||
|
|
|
@ -36,35 +36,13 @@ exports[`MessageItem when content is defined matches snapshot 1`] = `
|
|||
}
|
||||
}
|
||||
>
|
||||
<JSONTree
|
||||
collectionLimit={50}
|
||||
<JSONViewer
|
||||
data={
|
||||
Object {
|
||||
"foo": "bar",
|
||||
"key": "val",
|
||||
}
|
||||
}
|
||||
getItemString={[Function]}
|
||||
hideRoot={true}
|
||||
invertTheme={false}
|
||||
isCustomNode={[Function]}
|
||||
keyPath={
|
||||
Array [
|
||||
"root",
|
||||
]
|
||||
}
|
||||
labelRenderer={[Function]}
|
||||
postprocessValue={[Function]}
|
||||
shouldExpandNode={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"base0B": "#363636",
|
||||
"base0D": "#3273dc",
|
||||
"tree": [Function],
|
||||
"value": [Function],
|
||||
}
|
||||
}
|
||||
valueRenderer={[Function]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import React from 'react';
|
||||
import JSONTree from 'react-json-tree';
|
||||
import theme from './themes/google';
|
||||
|
||||
interface JSONViewerProps {
|
||||
data: {
|
||||
[key: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
const JSONViewer: React.FC<JSONViewerProps> = ({ data }) => (
|
||||
<JSONTree data={data} theme={theme} shouldExpandNode={() => true} hideRoot />
|
||||
);
|
||||
|
||||
export default JSONViewer;
|
|
@ -0,0 +1,20 @@
|
|||
export default {
|
||||
scheme: 'google',
|
||||
author: 'seth wright (http://sethawright.com)',
|
||||
base00: '#1d1f21',
|
||||
base01: '#282a2e',
|
||||
base02: '#373b41',
|
||||
base03: '#969896',
|
||||
base04: '#b4b7b4',
|
||||
base05: '#c5c8c6',
|
||||
base06: '#e0e0e0',
|
||||
base07: '#ffffff',
|
||||
base08: '#CC342B',
|
||||
base09: '#F96A38',
|
||||
base0A: '#FBA922',
|
||||
base0B: '#198844',
|
||||
base0C: '#3971ED',
|
||||
base0D: '#3971ED',
|
||||
base0E: '#A36AC7',
|
||||
base0F: '#3971ED',
|
||||
};
|
|
@ -10,6 +10,8 @@ export const clusterTopicNewPath = (clusterName: ClusterName) =>
|
|||
`${clusterPath(clusterName)}/topics/new`;
|
||||
export const clusterConsumerGroupsPath = (clusterName: ClusterName) =>
|
||||
`${clusterPath(clusterName)}/consumer-groups`;
|
||||
export const clusterSchemasPath = (clusterName: ClusterName) =>
|
||||
`${clusterPath(clusterName)}/schemas`;
|
||||
|
||||
export const clusterTopicPath = (
|
||||
clusterName: ClusterName,
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
import {
|
||||
clusterSchemasPayload,
|
||||
schemaVersionsPayload,
|
||||
} from 'redux/reducers/schemas/__test__/fixtures';
|
||||
import * as actions from '../actions';
|
||||
|
||||
describe('Actions', () => {
|
||||
|
@ -25,4 +29,50 @@ describe('Actions', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchSchemasByClusterNameAction', () => {
|
||||
it('creates a REQUEST action', () => {
|
||||
expect(actions.fetchSchemasByClusterNameAction.request()).toEqual({
|
||||
type: 'GET_CLUSTER_SCHEMAS__REQUEST',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a SUCCESS action', () => {
|
||||
expect(
|
||||
actions.fetchSchemasByClusterNameAction.success(clusterSchemasPayload)
|
||||
).toEqual({
|
||||
type: 'GET_CLUSTER_SCHEMAS__SUCCESS',
|
||||
payload: clusterSchemasPayload,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a FAILURE action', () => {
|
||||
expect(actions.fetchSchemasByClusterNameAction.failure()).toEqual({
|
||||
type: 'GET_CLUSTER_SCHEMAS__FAILURE',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchSchemaVersionsAction', () => {
|
||||
it('creates a REQUEST action', () => {
|
||||
expect(actions.fetchSchemaVersionsAction.request()).toEqual({
|
||||
type: 'GET_SCHEMA_VERSIONS__REQUEST',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a SUCCESS action', () => {
|
||||
expect(
|
||||
actions.fetchSchemaVersionsAction.success(schemaVersionsPayload)
|
||||
).toEqual({
|
||||
type: 'GET_SCHEMA_VERSIONS__SUCCESS',
|
||||
payload: schemaVersionsPayload,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a FAILURE action', () => {
|
||||
expect(actions.fetchSchemaVersionsAction.failure()).toEqual({
|
||||
type: 'GET_SCHEMA_VERSIONS__FAILURE',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,6 +8,7 @@ import { Middleware } from 'redux';
|
|||
import { RootState, Action } from 'redux/interfaces';
|
||||
import * as actions from 'redux/actions/actions';
|
||||
import * as thunks from 'redux/actions/thunks';
|
||||
import * as schemaFixtures from 'redux/reducers/schemas/__test__/fixtures';
|
||||
import * as fixtures from './fixtures';
|
||||
|
||||
const middlewares: Array<Middleware> = [thunk];
|
||||
|
@ -21,6 +22,7 @@ const mockStoreCreator: MockStoreCreator<
|
|||
const store: MockStoreEnhanced<RootState, DispatchExts> = mockStoreCreator();
|
||||
|
||||
const clusterName = 'local';
|
||||
const subject = 'test';
|
||||
|
||||
describe('Thunks', () => {
|
||||
afterEach(() => {
|
||||
|
@ -49,4 +51,58 @@ describe('Thunks', () => {
|
|||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchSchemasByClusterName', () => {
|
||||
it('creates GET_CLUSTER_SCHEMAS__SUCCESS when fetching cluster schemas', async () => {
|
||||
fetchMock.getOnce(`/api/clusters/${clusterName}/schemas`, {
|
||||
body: schemaFixtures.clusterSchemasPayload,
|
||||
});
|
||||
await store.dispatch(thunks.fetchSchemasByClusterName(clusterName));
|
||||
expect(store.getActions()).toEqual([
|
||||
actions.fetchSchemasByClusterNameAction.request(),
|
||||
actions.fetchSchemasByClusterNameAction.success(
|
||||
schemaFixtures.clusterSchemasPayload
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
it('creates GET_CLUSTER_SCHEMAS__FAILURE when fetching cluster schemas', async () => {
|
||||
fetchMock.getOnce(`/api/clusters/${clusterName}/schemas`, 404);
|
||||
await store.dispatch(thunks.fetchSchemasByClusterName(clusterName));
|
||||
expect(store.getActions()).toEqual([
|
||||
actions.fetchSchemasByClusterNameAction.request(),
|
||||
actions.fetchSchemasByClusterNameAction.failure(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchSchemaVersions', () => {
|
||||
it('creates GET_SCHEMA_VERSIONS__SUCCESS when fetching schema versions', async () => {
|
||||
fetchMock.getOnce(
|
||||
`/api/clusters/${clusterName}/schemas/${subject}/versions`,
|
||||
{
|
||||
body: schemaFixtures.schemaVersionsPayload,
|
||||
}
|
||||
);
|
||||
await store.dispatch(thunks.fetchSchemaVersions(clusterName, subject));
|
||||
expect(store.getActions()).toEqual([
|
||||
actions.fetchSchemaVersionsAction.request(),
|
||||
actions.fetchSchemaVersionsAction.success(
|
||||
schemaFixtures.schemaVersionsPayload
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
it('creates GET_SCHEMA_VERSIONS__FAILURE when fetching schema versions', async () => {
|
||||
fetchMock.getOnce(
|
||||
`/api/clusters/${clusterName}/schemas/${subject}/versions`,
|
||||
404
|
||||
);
|
||||
await store.dispatch(thunks.fetchSchemaVersions(clusterName, subject));
|
||||
expect(store.getActions()).toEqual([
|
||||
actions.fetchSchemaVersionsAction.request(),
|
||||
actions.fetchSchemaVersionsAction.failure(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
TopicMessage,
|
||||
ConsumerGroup,
|
||||
ConsumerGroupDetails,
|
||||
SchemaSubject,
|
||||
} from 'generated-sources';
|
||||
|
||||
export const fetchClusterStatsAction = createAsyncAction(
|
||||
|
@ -96,3 +97,15 @@ export const fetchConsumerGroupDetailsAction = createAsyncAction(
|
|||
{ consumerGroupID: ConsumerGroupID; details: ConsumerGroupDetails },
|
||||
undefined
|
||||
>();
|
||||
|
||||
export const fetchSchemasByClusterNameAction = createAsyncAction(
|
||||
'GET_CLUSTER_SCHEMAS__REQUEST',
|
||||
'GET_CLUSTER_SCHEMAS__SUCCESS',
|
||||
'GET_CLUSTER_SCHEMAS__FAILURE'
|
||||
)<undefined, SchemaSubject[], undefined>();
|
||||
|
||||
export const fetchSchemaVersionsAction = createAsyncAction(
|
||||
'GET_SCHEMA_VERSIONS__REQUEST',
|
||||
'GET_SCHEMA_VERSIONS__SUCCESS',
|
||||
'GET_SCHEMA_VERSIONS__FAILURE'
|
||||
)<undefined, SchemaSubject[], undefined>();
|
||||
|
|
|
@ -15,13 +15,14 @@ import {
|
|||
TopicMessageQueryParams,
|
||||
TopicFormFormattedParams,
|
||||
TopicFormDataRaw,
|
||||
SchemaName,
|
||||
} from 'redux/interfaces';
|
||||
|
||||
import { BASE_PARAMS } from 'lib/constants';
|
||||
import * as actions from './actions';
|
||||
|
||||
const apiClientConf = new Configuration(BASE_PARAMS);
|
||||
const apiClient = new ApiClustersApi(apiClientConf);
|
||||
export const apiClient = new ApiClustersApi(apiClientConf);
|
||||
|
||||
export const fetchClustersList = (): PromiseThunkResult => async (dispatch) => {
|
||||
dispatch(actions.fetchClusterListAction.request());
|
||||
|
@ -250,3 +251,32 @@ export const fetchConsumerGroupDetails = (
|
|||
dispatch(actions.fetchConsumerGroupDetailsAction.failure());
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchSchemasByClusterName = (
|
||||
clusterName: ClusterName
|
||||
): PromiseThunkResult<void> => async (dispatch) => {
|
||||
dispatch(actions.fetchSchemasByClusterNameAction.request());
|
||||
try {
|
||||
const schemas = await apiClient.getSchemas({ clusterName });
|
||||
dispatch(actions.fetchSchemasByClusterNameAction.success(schemas));
|
||||
} catch (e) {
|
||||
dispatch(actions.fetchSchemasByClusterNameAction.failure());
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchSchemaVersions = (
|
||||
clusterName: ClusterName,
|
||||
subject: SchemaName
|
||||
): PromiseThunkResult<void> => async (dispatch) => {
|
||||
if (!subject) return;
|
||||
dispatch(actions.fetchSchemaVersionsAction.request());
|
||||
try {
|
||||
const versions = await apiClient.getAllVersionsBySubject({
|
||||
clusterName,
|
||||
subject,
|
||||
});
|
||||
dispatch(actions.fetchSchemaVersionsAction.success(versions));
|
||||
} catch (e) {
|
||||
dispatch(actions.fetchSchemaVersionsAction.failure());
|
||||
}
|
||||
};
|
||||
|
|
|
@ -8,11 +8,13 @@ import { ClusterState } from './cluster';
|
|||
import { BrokersState } from './broker';
|
||||
import { LoaderState } from './loader';
|
||||
import { ConsumerGroupsState } from './consumerGroup';
|
||||
import { SchemasState } from './schema';
|
||||
|
||||
export * from './topic';
|
||||
export * from './cluster';
|
||||
export * from './broker';
|
||||
export * from './consumerGroup';
|
||||
export * from './schema';
|
||||
export * from './loader';
|
||||
|
||||
export interface RootState {
|
||||
|
@ -20,6 +22,7 @@ export interface RootState {
|
|||
clusters: ClusterState;
|
||||
brokers: BrokersState;
|
||||
consumerGroups: ConsumerGroupsState;
|
||||
schemas: SchemasState;
|
||||
loader: LoaderState;
|
||||
}
|
||||
|
||||
|
|
9
kafka-ui-react-app/src/redux/interfaces/schema.ts
Normal file
9
kafka-ui-react-app/src/redux/interfaces/schema.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { SchemaSubject } from 'generated-sources';
|
||||
|
||||
export type SchemaName = string;
|
||||
|
||||
export interface SchemasState {
|
||||
byName: { [subject: string]: SchemaSubject };
|
||||
allNames: SchemaName[];
|
||||
currentSchemaVersions: SchemaSubject[];
|
||||
}
|
|
@ -4,6 +4,7 @@ import topics from './topics/reducer';
|
|||
import clusters from './clusters/reducer';
|
||||
import brokers from './brokers/reducer';
|
||||
import consumerGroups from './consumerGroups/reducer';
|
||||
import schemas from './schemas/reducer';
|
||||
import loader from './loader/reducer';
|
||||
|
||||
export default combineReducers<RootState>({
|
||||
|
@ -11,5 +12,6 @@ export default combineReducers<RootState>({
|
|||
clusters,
|
||||
brokers,
|
||||
consumerGroups,
|
||||
schemas,
|
||||
loader,
|
||||
});
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Schemas reducer reacts on GET_CLUSTER_SCHEMAS__SUCCESS and returns payload 1`] = `
|
||||
Object {
|
||||
"allNames": Array [
|
||||
"test2",
|
||||
"test3",
|
||||
"test",
|
||||
],
|
||||
"byName": Object {
|
||||
"test": Object {
|
||||
"compatibilityLevel": "BACKWARD",
|
||||
"id": 2,
|
||||
"schema": "{\\"type\\":\\"record\\",\\"name\\":\\"MyRecord2\\",\\"namespace\\":\\"com.mycompany\\",\\"fields\\":[{\\"name\\":\\"id\\",\\"type\\":\\"long\\"}]}",
|
||||
"subject": "test",
|
||||
"version": "2",
|
||||
},
|
||||
"test2": Object {
|
||||
"compatibilityLevel": "BACKWARD",
|
||||
"id": 4,
|
||||
"schema": "{\\"type\\":\\"record\\",\\"name\\":\\"MyRecord4\\",\\"namespace\\":\\"com.mycompany\\",\\"fields\\":[{\\"name\\":\\"id\\",\\"type\\":\\"long\\"}]}",
|
||||
"subject": "test2",
|
||||
"version": "3",
|
||||
},
|
||||
"test3": Object {
|
||||
"compatibilityLevel": "BACKWARD",
|
||||
"id": 5,
|
||||
"schema": "{\\"type\\":\\"record\\",\\"name\\":\\"MyRecord\\",\\"namespace\\":\\"com.mycompany\\",\\"fields\\":[{\\"name\\":\\"id\\",\\"type\\":\\"long\\"}]}",
|
||||
"subject": "test3",
|
||||
"version": "1",
|
||||
},
|
||||
},
|
||||
"currentSchemaVersions": Array [],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Schemas reducer reacts on GET_SCHEMA_VERSIONS__SUCCESS and returns payload 1`] = `
|
||||
Object {
|
||||
"allNames": Array [],
|
||||
"byName": Object {},
|
||||
"currentSchemaVersions": Array [
|
||||
Object {
|
||||
"compatibilityLevel": "BACKWARD",
|
||||
"id": 1,
|
||||
"schema": "{\\"type\\":\\"record\\",\\"name\\":\\"MyRecord1\\",\\"namespace\\":\\"com.mycompany\\",\\"fields\\":[{\\"name\\":\\"id\\",\\"type\\":\\"long\\"}]}",
|
||||
"subject": "test",
|
||||
"version": "1",
|
||||
},
|
||||
Object {
|
||||
"compatibilityLevel": "BACKWARD",
|
||||
"id": 2,
|
||||
"schema": "{\\"type\\":\\"record\\",\\"name\\":\\"MyRecord2\\",\\"namespace\\":\\"com.mycompany\\",\\"fields\\":[{\\"name\\":\\"id\\",\\"type\\":\\"long\\"}]}",
|
||||
"subject": "test",
|
||||
"version": "2",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,54 @@
|
|||
import { SchemasState } from 'redux/interfaces';
|
||||
import { SchemaSubject } from 'generated-sources';
|
||||
|
||||
export const initialState: SchemasState = {
|
||||
byName: {},
|
||||
allNames: [],
|
||||
currentSchemaVersions: [],
|
||||
};
|
||||
|
||||
export const clusterSchemasPayload: 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',
|
||||
},
|
||||
];
|
||||
|
||||
export const schemaVersionsPayload: SchemaSubject[] = [
|
||||
{
|
||||
subject: 'test',
|
||||
version: '1',
|
||||
id: 1,
|
||||
schema:
|
||||
'{"type":"record","name":"MyRecord1","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',
|
||||
},
|
||||
];
|
|
@ -0,0 +1,39 @@
|
|||
import {
|
||||
fetchSchemasByClusterNameAction,
|
||||
fetchSchemaVersionsAction,
|
||||
} from 'redux/actions';
|
||||
import reducer from 'redux/reducers/schemas/reducer';
|
||||
import {
|
||||
clusterSchemasPayload,
|
||||
initialState,
|
||||
schemaVersionsPayload,
|
||||
} from './fixtures';
|
||||
|
||||
describe('Schemas reducer', () => {
|
||||
it('returns the initial state', () => {
|
||||
expect(
|
||||
reducer(undefined, fetchSchemasByClusterNameAction.request())
|
||||
).toEqual(initialState);
|
||||
expect(reducer(undefined, fetchSchemaVersionsAction.request())).toEqual(
|
||||
initialState
|
||||
);
|
||||
});
|
||||
|
||||
it('reacts on GET_CLUSTER_SCHEMAS__SUCCESS and returns payload', () => {
|
||||
expect(
|
||||
reducer(
|
||||
undefined,
|
||||
fetchSchemasByClusterNameAction.success(clusterSchemasPayload)
|
||||
)
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('reacts on GET_SCHEMA_VERSIONS__SUCCESS and returns payload', () => {
|
||||
expect(
|
||||
reducer(
|
||||
undefined,
|
||||
fetchSchemaVersionsAction.success(schemaVersionsPayload)
|
||||
)
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,64 @@
|
|||
import {
|
||||
fetchSchemasByClusterNameAction,
|
||||
fetchSchemaVersionsAction,
|
||||
} from 'redux/actions';
|
||||
import configureStore from 'redux/store/configureStore';
|
||||
import * as selectors from '../selectors';
|
||||
import { clusterSchemasPayload, schemaVersionsPayload } from './fixtures';
|
||||
|
||||
const store = configureStore();
|
||||
|
||||
describe('Schemas selectors', () => {
|
||||
describe('Initial state', () => {
|
||||
it('returns fetch status', () => {
|
||||
expect(selectors.getIsSchemaListFetched(store.getState())).toBeFalsy();
|
||||
expect(selectors.getIsSchemaVersionFetched(store.getState())).toBeFalsy();
|
||||
});
|
||||
|
||||
it('returns schema list', () => {
|
||||
expect(selectors.getSchemaList(store.getState())).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns undefined schema', () => {
|
||||
expect(selectors.getSchema(store.getState(), ' ')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns sorted versions of schema', () => {
|
||||
expect(selectors.getSortedSchemaVersions(store.getState())).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('state', () => {
|
||||
beforeAll(() => {
|
||||
store.dispatch(
|
||||
fetchSchemasByClusterNameAction.success(clusterSchemasPayload)
|
||||
);
|
||||
store.dispatch(fetchSchemaVersionsAction.success(schemaVersionsPayload));
|
||||
});
|
||||
|
||||
it('returns fetch status', () => {
|
||||
expect(selectors.getIsSchemaListFetched(store.getState())).toBeTruthy();
|
||||
expect(
|
||||
selectors.getIsSchemaVersionFetched(store.getState())
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns schema list', () => {
|
||||
expect(selectors.getSchemaList(store.getState())).toEqual(
|
||||
clusterSchemasPayload
|
||||
);
|
||||
});
|
||||
|
||||
it('returns schema', () => {
|
||||
expect(selectors.getSchema(store.getState(), 'test2')).toEqual(
|
||||
clusterSchemasPayload[0]
|
||||
);
|
||||
});
|
||||
|
||||
it('returns sorted versions of schema', () => {
|
||||
expect(selectors.getSortedSchemaVersions(store.getState())).toEqual(
|
||||
schemaVersionsPayload
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
46
kafka-ui-react-app/src/redux/reducers/schemas/reducer.ts
Normal file
46
kafka-ui-react-app/src/redux/reducers/schemas/reducer.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { SchemaSubject } from 'generated-sources';
|
||||
import { Action, SchemasState } from 'redux/interfaces';
|
||||
|
||||
export const initialState: SchemasState = {
|
||||
byName: {},
|
||||
allNames: [],
|
||||
currentSchemaVersions: [],
|
||||
};
|
||||
|
||||
const updateSchemaList = (
|
||||
state: SchemasState,
|
||||
payload: SchemaSubject[]
|
||||
): SchemasState => {
|
||||
const initialMemo: SchemasState = {
|
||||
...state,
|
||||
allNames: [],
|
||||
};
|
||||
|
||||
return payload.reduce((memo: SchemasState, schema) => {
|
||||
if (!schema.subject) return memo;
|
||||
return {
|
||||
...memo,
|
||||
byName: {
|
||||
...memo.byName,
|
||||
[schema.subject]: {
|
||||
...memo.byName[schema.subject],
|
||||
...schema,
|
||||
},
|
||||
},
|
||||
allNames: [...memo.allNames, schema.subject],
|
||||
};
|
||||
}, initialMemo);
|
||||
};
|
||||
|
||||
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 };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default reducer;
|
48
kafka-ui-react-app/src/redux/reducers/schemas/selectors.ts
Normal file
48
kafka-ui-react-app/src/redux/reducers/schemas/selectors.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import { RootState, SchemasState } from 'redux/interfaces';
|
||||
import { createFetchingSelector } from 'redux/reducers/loader/selectors';
|
||||
|
||||
const schemasState = ({ schemas }: RootState): SchemasState => schemas;
|
||||
|
||||
const getAllNames = (state: RootState) => schemasState(state).allNames;
|
||||
const getSchemaMap = (state: RootState) => schemasState(state).byName;
|
||||
|
||||
const getSchemaListFetchingStatus = createFetchingSelector(
|
||||
'GET_CLUSTER_SCHEMAS'
|
||||
);
|
||||
|
||||
const getSchemaVersionsFetchingStatus = createFetchingSelector(
|
||||
'GET_SCHEMA_VERSIONS'
|
||||
);
|
||||
|
||||
export const getIsSchemaListFetched = createSelector(
|
||||
getSchemaListFetchingStatus,
|
||||
(status) => status === 'fetched'
|
||||
);
|
||||
|
||||
export const getIsSchemaVersionFetched = createSelector(
|
||||
getSchemaVersionsFetchingStatus,
|
||||
(status) => status === 'fetched'
|
||||
);
|
||||
|
||||
export const getSchemaList = createSelector(
|
||||
getIsSchemaListFetched,
|
||||
getAllNames,
|
||||
getSchemaMap,
|
||||
(isFetched, allNames, byName) =>
|
||||
isFetched ? allNames.map((subject) => byName[subject]) : []
|
||||
);
|
||||
|
||||
const getSchemaName = (_: RootState, subject: string) => subject;
|
||||
|
||||
export const getSchema = createSelector(
|
||||
getSchemaMap,
|
||||
getSchemaName,
|
||||
(schemas, subject) => schemas[subject]
|
||||
);
|
||||
|
||||
export const getSortedSchemaVersions = createSelector(
|
||||
schemasState,
|
||||
({ currentSchemaVersions }) =>
|
||||
currentSchemaVersions.sort((a, b) => a.id - b.id)
|
||||
);
|
Loading…
Add table
Reference in a new issue