Schema show page (#196)

* Latest version added

* Changes from [PR-186] applied

* Schema Versions get method added

* Schema show page created

* Updated JSONViewer

* Details component refactored

* Tests for Messages component updated

* Details component and packages updated

* Feedback

* Delete testSchema

Co-authored-by: Oleg Shuralev <workshur@gmail.com>
This commit is contained in:
Guzel738 2021-02-18 21:59:04 +03:00 committed by GitHub
parent 2cf2f6e186
commit 44949b9af2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 358 additions and 81 deletions

View file

@ -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

View file

@ -4962,9 +4962,9 @@
"dev": true
},
"bulma": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/bulma/-/bulma-0.8.2.tgz",
"integrity": "sha512-vMM/ijYSxX+Sm+nD7Lmc1UgWDy2JcL2nTKqwgEqXuOMU+IGALbXd5MLt/BcjBAPLIx36TtzhzBcSnOP974gcqA=="
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.2.tgz",
"integrity": "sha512-e14EF+3VSZ488yL/lJH0tR8mFWiEQVCMi/BQUMi2TGMBOk+zrDg4wryuwm/+dRSHJw0gMawp2tsW7X1JYUCE3A=="
},
"bulma-switch": {
"version": "2.0.0",

View file

@ -3,7 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"bulma": "^0.8.2",
"bulma": "^0.9.2",
"bulma-switch": "^2.0.0",
"classnames": "^2.2.6",
"date-fns": "^2.16.1",
@ -112,6 +112,8 @@
},
"proxy": "http://localhost:8080",
"jest": {
"snapshotSerializers": ["enzyme-to-json/serializer"]
"snapshotSerializers": [
"enzyme-to-json/serializer"
]
}
}

View 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';
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;

View file

@ -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)
);

View file

@ -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;

View file

@ -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;

View file

@ -1,5 +1,5 @@
import { SchemaSubject } from 'generated-sources';
import React from 'react';
import { SchemaSubject } from 'generated-sources';
import Breadcrumb from '../../common/Breadcrumb/Breadcrumb';
import ListItem from './ListItem';
@ -16,11 +16,13 @@ const List: React.FC<ListProps> = ({ schemas }) => {
<thead>
<tr>
<th>Schema Name</th>
<th>Version</th>
<th>Compatibility</th>
</tr>
</thead>
<tbody>
{schemas.map(({ subject }) => (
<ListItem subject={subject} />
{schemas.map((subject) => (
<ListItem key={subject.id} subject={subject} />
))}
</tbody>
</table>

View file

@ -1,13 +1,30 @@
import React from 'react';
import { SchemaSubject } from 'generated-sources';
import { NavLink } from 'react-router-dom';
interface ListItemProps {
subject?: string;
subject: SchemaSubject;
}
const ListItem: React.FC<ListItemProps> = ({ subject }) => {
const ListItem: React.FC<ListItemProps> = ({
subject: { subject, version, compatibilityLevel },
}) => {
return (
<tr>
<td>{subject}</td>
<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>
);
};

View file

@ -3,6 +3,7 @@ import { ClusterName } from 'redux/interfaces';
import { Switch, Route, useParams } from 'react-router-dom';
import PageLoader from 'components/common/PageLoader/PageLoader';
import ListContainer from './List/ListContainer';
import DetailsContainer from './Details/DetailsContainer';
interface SchemasProps {
isFetched: boolean;
@ -31,6 +32,11 @@ const Schemas: React.FC<SchemasProps> = ({
path="/ui/clusters/:clusterName/schemas"
component={ListContainer}
/>
<Route
exact
path="/ui/clusters/:clusterName/schemas/:subject/latest"
component={DetailsContainer}
/>
</Switch>
);
}

View file

@ -1,8 +1,8 @@
import { connect } from 'react-redux';
import { RootState } from '../../redux/interfaces';
import { fetchSchemasByClusterName } from '../../redux/actions';
import { RootState } from 'redux/interfaces';
import { fetchSchemasByClusterName } from 'redux/actions';
import { getIsSchemaListFetched } from 'redux/reducers/schemas/selectors';
import Schemas from './Schemas';
import { getIsSchemaListFetched } from '../../redux/reducers/schemas/selectors';
const mapStateToProps = (state: RootState) => ({
isFetched: getIsSchemaListFetched(state),

View file

@ -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>
);

View file

@ -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 }}
/>
)
)}

View file

@ -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', () => {

View file

@ -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>

View file

@ -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;

View file

@ -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',
};

View file

@ -103,3 +103,9 @@ export const fetchSchemasByClusterNameAction = createAsyncAction(
'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>();

View file

@ -5,7 +5,6 @@ import {
Topic,
TopicFormData,
TopicConfig,
SchemaSubject,
} from 'generated-sources';
import {
ConsumerGroupID,
@ -16,6 +15,7 @@ import {
TopicMessageQueryParams,
TopicFormFormattedParams,
TopicFormDataRaw,
SchemaName,
} from 'redux/interfaces';
import { BASE_PARAMS } from 'lib/constants';
@ -257,17 +257,26 @@ export const fetchSchemasByClusterName = (
): PromiseThunk<void> => async (dispatch) => {
dispatch(actions.fetchSchemasByClusterNameAction.request());
try {
const schemaNames = await apiClient.getSchemas({ clusterName });
// TODO: Remove me after API refactoring
const schemas: SchemaSubject[] = await Promise.all(
schemaNames.map((schemaName) =>
apiClient.getLatestSchema({ clusterName, schemaName })
)
);
const schemas = await apiClient.getSchemas({ clusterName });
dispatch(actions.fetchSchemasByClusterNameAction.success(schemas));
} catch (e) {
dispatch(actions.fetchSchemasByClusterNameAction.failure());
}
};
export const fetchSchemaVersions = (
clusterName: ClusterName,
subject: SchemaName
): PromiseThunk<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());
}
};

View file

@ -1,6 +1,9 @@
import { SchemaSubject } from 'generated-sources';
export type SchemaName = string;
export interface SchemasState {
byName: { [name: string]: SchemaSubject };
allNames: string[];
byName: { [subject: string]: SchemaSubject };
allNames: SchemaName[];
currentSchemaVersions: SchemaSubject[];
}

View file

@ -4,6 +4,7 @@ import { Action, SchemasState } from 'redux/interfaces';
export const initialState: SchemasState = {
byName: {},
allNames: [],
currentSchemaVersions: [],
};
const updateSchemaList = (
@ -35,6 +36,8 @@ 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;
}

View file

@ -11,19 +11,38 @@ 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) => {
if (!isFetched) {
return [];
}
return allNames.map((subject) => byName[subject]);
}
(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)
);

View file

@ -2,6 +2,7 @@
@import "../../node_modules/bulma/sass/base/_all.sass";
@import "../../node_modules/bulma/sass/elements/_all.sass";
@import "../../node_modules/bulma/sass/form/_all.sass";
@import "../../node_modules/bulma/sass/helpers/_all.sass";
@import "../../node_modules/bulma/sass/components/_all.sass";
@import "../../node_modules/bulma/sass/grid/_all.sass";
@import "../../node_modules/bulma/sass/layout/_all.sass";