* #867: add a view for comparing schema versions * fixing test for diff component * adding color to lines which are different in versions * adding function to determine schema type and display correct format when comparing versions * removing unneccessary style * changing fetch approach and fixing test issue * fixinf schema versions comparision path change approach * remove unnecessary code * removing enzyme,removing direct use of Colors and adding dispatch to array of deps * added requested changes * makeing requested changes Co-authored-by: NelyDavtyan <ndavtyan@provectus.com> Co-authored-by: Roman Zabaluev <rzabaluev@provectus.com> Co-authored-by: NelyDavtyan <96067981+NelyDavtyan@users.noreply.github.com>
This commit is contained in:
parent
4cc4175ef2
commit
dd42dbe0cd
11 changed files with 562 additions and 3 deletions
|
@ -1,6 +1,10 @@
|
|||
import React from 'react';
|
||||
import { useHistory, useParams } from 'react-router';
|
||||
import { clusterSchemasPath, clusterSchemaEditPath } from 'lib/paths';
|
||||
import {
|
||||
clusterSchemasPath,
|
||||
clusterSchemaSchemaDiffPath,
|
||||
clusterSchemaEditPath,
|
||||
} from 'lib/paths';
|
||||
import ClusterContext from 'components/contexts/ClusterContext';
|
||||
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
|
||||
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||
|
@ -77,12 +81,22 @@ const Details: React.FC = () => {
|
|||
if (!isFetched || !schema) {
|
||||
return <PageLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeading text={schema.subject}>
|
||||
{!isReadOnly && (
|
||||
<>
|
||||
<Button
|
||||
isLink
|
||||
buttonSize="M"
|
||||
buttonType="primary"
|
||||
to={{
|
||||
pathname: clusterSchemaSchemaDiffPath(clusterName, subject),
|
||||
search: `leftVersion=${versions[0]?.version}&rightVersion=${versions[0]?.version}`,
|
||||
}}
|
||||
>
|
||||
Compare Versions
|
||||
</Button>
|
||||
<Button
|
||||
isLink
|
||||
buttonSize="M"
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
import styled from 'styled-components';
|
||||
|
||||
export const DiffWrapper = styled.div`
|
||||
align-items: stretch;
|
||||
display: block;
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-height: min-content;
|
||||
padding-top: 1.5rem !important;
|
||||
&
|
||||
.ace_editor
|
||||
> .ace_scroller
|
||||
> .ace_content
|
||||
> .ace_marker-layer
|
||||
> .codeMarker {
|
||||
background: ${({ theme }) => theme.icons.warningIcon};
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Section = styled.div`
|
||||
animation: fadein 0.5s;
|
||||
`;
|
||||
|
||||
export const DiffBox = styled.div`
|
||||
flex-direction: column;
|
||||
margin-left: -0.75rem;
|
||||
margin-right: -0.75rem;
|
||||
margin-top: -0.75rem;
|
||||
box-shadow: none;
|
||||
padding: 1.25rem;
|
||||
&:last-child {
|
||||
margin-bottom: -0.75rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const DiffTilesWrapper = styled.div`
|
||||
align-items: stretch;
|
||||
display: block;
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-height: min-content;
|
||||
&:not(.is-child) {
|
||||
display: flex;
|
||||
}
|
||||
`;
|
||||
|
||||
export const DiffTile = styled.div`
|
||||
flex: none;
|
||||
width: 50%;
|
||||
`;
|
||||
|
||||
export const DiffVersionsSelect = styled.div`
|
||||
width: 0.625em;
|
||||
`;
|
179
kafka-ui-react-app/src/components/Schemas/Diff/Diff.tsx
Normal file
179
kafka-ui-react-app/src/components/Schemas/Diff/Diff.tsx
Normal file
|
@ -0,0 +1,179 @@
|
|||
import React from 'react';
|
||||
import { SchemaSubject } from 'generated-sources';
|
||||
import { clusterSchemaSchemaDiffPath } from 'lib/paths';
|
||||
import PageLoader from 'components/common/PageLoader/PageLoader';
|
||||
import DiffViewer from 'components/common/DiffViewer/DiffViewer';
|
||||
import { useHistory, useParams, useLocation } from 'react-router';
|
||||
import {
|
||||
fetchSchemaVersions,
|
||||
SCHEMAS_VERSIONS_FETCH_ACTION,
|
||||
} from 'redux/reducers/schemas/schemasSlice';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import Select from 'components/common/Select/Select';
|
||||
import { useAppDispatch } from 'lib/hooks/redux';
|
||||
import { resetLoaderById } from 'redux/reducers/loader/loaderSlice';
|
||||
|
||||
import * as S from './Diff.styled';
|
||||
|
||||
export interface DiffProps {
|
||||
leftVersionInPath?: string;
|
||||
rightVersionInPath?: string;
|
||||
versions: SchemaSubject[];
|
||||
areVersionsFetched: boolean;
|
||||
}
|
||||
|
||||
const Diff: React.FC<DiffProps> = ({
|
||||
leftVersionInPath,
|
||||
rightVersionInPath,
|
||||
versions,
|
||||
areVersionsFetched,
|
||||
}) => {
|
||||
const [leftVersion, setLeftVersion] = React.useState(leftVersionInPath || '');
|
||||
const [rightVersion, setRightVersion] = React.useState(
|
||||
rightVersionInPath || ''
|
||||
);
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
|
||||
const { clusterName, subject } =
|
||||
useParams<{ clusterName: string; subject: string }>();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
React.useEffect(() => {
|
||||
dispatch(fetchSchemaVersions({ clusterName, subject }));
|
||||
return () => {
|
||||
dispatch(resetLoaderById(SCHEMAS_VERSIONS_FETCH_ACTION));
|
||||
};
|
||||
}, [clusterName, subject, dispatch]);
|
||||
|
||||
const getSchemaContent = (allVersions: SchemaSubject[], version: string) => {
|
||||
const selectedSchema =
|
||||
allVersions.find((s) => s.version === version)?.schema ||
|
||||
(allVersions.length ? allVersions[0].schema : '');
|
||||
return selectedSchema.trim().startsWith('{')
|
||||
? JSON.stringify(JSON.parse(selectedSchema), null, '\t')
|
||||
: selectedSchema;
|
||||
};
|
||||
const getSchemaType = (allVersions: SchemaSubject[]) => {
|
||||
return allVersions[0].schemaType;
|
||||
};
|
||||
|
||||
const methods = useForm({ mode: 'onChange' });
|
||||
const {
|
||||
formState: { isSubmitting },
|
||||
control,
|
||||
} = methods;
|
||||
|
||||
const searchParams = React.useMemo(
|
||||
() => new URLSearchParams(location.search),
|
||||
[location]
|
||||
);
|
||||
|
||||
return (
|
||||
<S.Section>
|
||||
{areVersionsFetched ? (
|
||||
<S.DiffBox>
|
||||
<S.DiffTilesWrapper>
|
||||
<S.DiffTile>
|
||||
<S.DiffVersionsSelect>
|
||||
<Controller
|
||||
defaultValue={leftVersion}
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
name="schemaType"
|
||||
render={({ field: { name } }) => (
|
||||
<Select
|
||||
id="left-select"
|
||||
name={name}
|
||||
value={
|
||||
leftVersion === '' ? versions[0].version : leftVersion
|
||||
}
|
||||
onChange={(event) => {
|
||||
history.push(
|
||||
clusterSchemaSchemaDiffPath(clusterName, subject)
|
||||
);
|
||||
searchParams.set('leftVersion', event.toString());
|
||||
searchParams.set(
|
||||
'rightVersion',
|
||||
rightVersion === ''
|
||||
? versions[0].version
|
||||
: rightVersion
|
||||
);
|
||||
history.push({
|
||||
search: `?${searchParams.toString()}`,
|
||||
});
|
||||
setLeftVersion(event.toString());
|
||||
}}
|
||||
minWidth="100%"
|
||||
disabled={isSubmitting}
|
||||
options={versions.map((type) => ({
|
||||
value: type.version,
|
||||
label: `Version ${type.version}`,
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</S.DiffVersionsSelect>
|
||||
</S.DiffTile>
|
||||
<S.DiffTile>
|
||||
<S.DiffVersionsSelect>
|
||||
<Controller
|
||||
defaultValue={rightVersion}
|
||||
control={control}
|
||||
rules={{ required: true }}
|
||||
name="schemaType"
|
||||
render={({ field: { name } }) => (
|
||||
<Select
|
||||
id="right-select"
|
||||
name={name}
|
||||
value={
|
||||
rightVersion === '' ? versions[0].version : rightVersion
|
||||
}
|
||||
onChange={(event) => {
|
||||
history.push(
|
||||
clusterSchemaSchemaDiffPath(clusterName, subject)
|
||||
);
|
||||
searchParams.set(
|
||||
'leftVersion',
|
||||
leftVersion === '' ? versions[0].version : leftVersion
|
||||
);
|
||||
searchParams.set('rightVersion', event.toString());
|
||||
history.push({
|
||||
search: `?${searchParams.toString()}`,
|
||||
});
|
||||
setRightVersion(event.toString());
|
||||
}}
|
||||
minWidth="100%"
|
||||
disabled={isSubmitting}
|
||||
options={versions.map((type) => ({
|
||||
value: type.version,
|
||||
label: `Version ${type.version}`,
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</S.DiffVersionsSelect>
|
||||
</S.DiffTile>
|
||||
</S.DiffTilesWrapper>
|
||||
<S.DiffWrapper>
|
||||
<DiffViewer
|
||||
value={[
|
||||
getSchemaContent(versions, leftVersion),
|
||||
getSchemaContent(versions, rightVersion),
|
||||
]}
|
||||
setOptions={{
|
||||
autoScrollEditorIntoView: true,
|
||||
}}
|
||||
isFixedHeight={false}
|
||||
schemaType={getSchemaType(versions)}
|
||||
/>
|
||||
</S.DiffWrapper>
|
||||
</S.DiffBox>
|
||||
) : (
|
||||
<PageLoader />
|
||||
)}
|
||||
</S.Section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Diff;
|
|
@ -0,0 +1,32 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { ClusterName, RootState } from 'redux/interfaces';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
import {
|
||||
getAreSchemaVersionsFulfilled,
|
||||
selectAllSchemaVersions,
|
||||
} from 'redux/reducers/schemas/schemasSlice';
|
||||
|
||||
import Diff from './Diff';
|
||||
|
||||
interface RouteProps {
|
||||
leftVersion?: string;
|
||||
rightVersion?: string;
|
||||
}
|
||||
|
||||
type OwnProps = RouteComponentProps<RouteProps>;
|
||||
|
||||
const mapStateToProps = (
|
||||
state: RootState,
|
||||
{
|
||||
match: {
|
||||
params: { leftVersion, rightVersion },
|
||||
},
|
||||
}: OwnProps
|
||||
) => ({
|
||||
versions: selectAllSchemaVersions(state),
|
||||
areVersionsFetched: getAreSchemaVersionsFulfilled(state),
|
||||
leftVersionInPath: leftVersion,
|
||||
rightVersionInPath: rightVersion,
|
||||
});
|
||||
|
||||
export default withRouter(connect(mapStateToProps)(Diff));
|
|
@ -0,0 +1,127 @@
|
|||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { StaticRouter } from 'react-router';
|
||||
import Diff, { DiffProps } from 'components/Schemas/Diff/Diff';
|
||||
import { render } from 'lib/testHelpers';
|
||||
import { screen } from '@testing-library/react';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import { versions } from './fixtures';
|
||||
|
||||
const middlewares = [thunk];
|
||||
const mockStore = configureStore(middlewares);
|
||||
|
||||
describe('Diff', () => {
|
||||
const initialState: Partial<DiffProps> = {};
|
||||
const store = mockStore(initialState);
|
||||
|
||||
const setupComponent = (props: DiffProps) =>
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<StaticRouter>
|
||||
<Diff
|
||||
versions={props.versions}
|
||||
leftVersionInPath={props.leftVersionInPath}
|
||||
rightVersionInPath={props.rightVersionInPath}
|
||||
areVersionsFetched={props.areVersionsFetched}
|
||||
/>
|
||||
</StaticRouter>
|
||||
</Provider>
|
||||
);
|
||||
describe('Container', () => {
|
||||
it('renders view', () => {
|
||||
setupComponent({
|
||||
areVersionsFetched: true,
|
||||
versions,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('View', () => {
|
||||
setupComponent({
|
||||
areVersionsFetched: true,
|
||||
versions,
|
||||
});
|
||||
});
|
||||
describe('when page with schema versions is loading', () => {
|
||||
beforeAll(() => {
|
||||
setupComponent({
|
||||
areVersionsFetched: false,
|
||||
versions: [],
|
||||
});
|
||||
});
|
||||
it('renders PageLoader', () => {
|
||||
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when schema versions are loaded and no specified versions in path', () => {
|
||||
beforeEach(() => {
|
||||
setupComponent({
|
||||
areVersionsFetched: true,
|
||||
versions,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders all options', () => {
|
||||
const selectedOption = screen.getAllByRole('option');
|
||||
expect(selectedOption.length).toEqual(2);
|
||||
});
|
||||
it('renders left select with empty value', () => {
|
||||
const select = screen.getAllByRole('listbox')[0];
|
||||
expect(select).toBeInTheDocument();
|
||||
expect(select).toHaveTextContent(versions[0].version);
|
||||
});
|
||||
|
||||
it('renders right select with empty value', () => {
|
||||
const select = screen.getAllByRole('listbox')[1];
|
||||
expect(select).toBeInTheDocument();
|
||||
expect(select).toHaveTextContent(versions[0].version);
|
||||
});
|
||||
});
|
||||
describe('when schema versions are loaded and two versions in path', () => {
|
||||
beforeEach(() => {
|
||||
setupComponent({
|
||||
areVersionsFetched: true,
|
||||
versions,
|
||||
leftVersionInPath: '1',
|
||||
rightVersionInPath: '2',
|
||||
});
|
||||
});
|
||||
|
||||
it('renders left select with version 1', () => {
|
||||
const select = screen.getAllByRole('listbox')[0];
|
||||
expect(select).toBeInTheDocument();
|
||||
expect(select).toHaveTextContent('1');
|
||||
});
|
||||
|
||||
it('renders right select with version 2', () => {
|
||||
const select = screen.getAllByRole('listbox')[1];
|
||||
expect(select).toBeInTheDocument();
|
||||
expect(select).toHaveTextContent('2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when schema versions are loaded and only one versions in path', () => {
|
||||
beforeEach(() => {
|
||||
setupComponent({
|
||||
areVersionsFetched: true,
|
||||
versions,
|
||||
leftVersionInPath: '1',
|
||||
});
|
||||
});
|
||||
|
||||
it('renders left select with version 1', () => {
|
||||
const select = screen.getAllByRole('listbox')[0];
|
||||
expect(select).toBeInTheDocument();
|
||||
expect(select).toHaveTextContent('1');
|
||||
});
|
||||
|
||||
it('renders right select with empty value', () => {
|
||||
const select = screen.getAllByRole('listbox')[1];
|
||||
expect(select).toBeInTheDocument();
|
||||
expect(select).toHaveTextContent(versions[0].version);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
import { SchemaSubject, SchemaType } from 'generated-sources';
|
||||
|
||||
export const versions: SchemaSubject[] = [
|
||||
{
|
||||
subject: 'test',
|
||||
version: '3',
|
||||
id: 3,
|
||||
schema:
|
||||
'syntax = "proto3";\npackage com.indeed;\n\nmessage MyRecord {\n int32 id = 1;\n string name = 2;\n}\n',
|
||||
compatibilityLevel: 'BACKWARD',
|
||||
schemaType: SchemaType.PROTOBUF,
|
||||
},
|
||||
{
|
||||
subject: 'test',
|
||||
version: '2',
|
||||
id: 2,
|
||||
schema:
|
||||
'{"type":"record","name":"MyRecord2","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
|
||||
compatibilityLevel: 'BACKWARD',
|
||||
schemaType: SchemaType.JSON,
|
||||
},
|
||||
{
|
||||
subject: 'test',
|
||||
version: '1',
|
||||
id: 1,
|
||||
schema:
|
||||
'{"type":"record","name":"MyRecord1","namespace":"com.mycompany","fields":[{"name":"id","type":"long"}]}',
|
||||
compatibilityLevel: 'BACKWARD',
|
||||
schemaType: SchemaType.JSON,
|
||||
},
|
||||
];
|
|
@ -5,11 +5,13 @@ import {
|
|||
clusterSchemaPath,
|
||||
clusterSchemaEditPath,
|
||||
clusterSchemasPath,
|
||||
clusterSchemaSchemaDiffPath,
|
||||
} from 'lib/paths';
|
||||
import List from 'components/Schemas/List/List';
|
||||
import Details from 'components/Schemas/Details/Details';
|
||||
import New from 'components/Schemas/New/New';
|
||||
import Edit from 'components/Schemas/Edit/Edit';
|
||||
import DiffContainer from 'components/Schemas/Diff/DiffContainer';
|
||||
import { BreadcrumbRoute } from 'components/common/Breadcrumb/Breadcrumb.route';
|
||||
|
||||
const Schemas: React.FC = () => {
|
||||
|
@ -35,6 +37,11 @@ const Schemas: React.FC = () => {
|
|||
path={clusterSchemaEditPath(':clusterName', ':subject')}
|
||||
component={Edit}
|
||||
/>
|
||||
<BreadcrumbRoute
|
||||
exact
|
||||
path={clusterSchemaSchemaDiffPath(':clusterName', ':subject')}
|
||||
component={DiffContainer}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,7 +8,7 @@ interface Props
|
|||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
ButtonProps {
|
||||
isLink?: boolean;
|
||||
to?: string;
|
||||
to?: string | object;
|
||||
}
|
||||
|
||||
export const Button: React.FC<Props> = ({ isLink, to, ...props }) => {
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
import { diff as DiffEditor } from 'react-ace';
|
||||
import 'ace-builds/src-noconflict/mode-json5';
|
||||
import 'ace-builds/src-noconflict/mode-protobuf';
|
||||
import 'ace-builds/src-noconflict/theme-textmate';
|
||||
import React from 'react';
|
||||
import { IDiffEditorProps } from 'react-ace/lib/diff';
|
||||
import { SchemaType } from 'generated-sources';
|
||||
|
||||
interface DiffViewerProps extends IDiffEditorProps {
|
||||
isFixedHeight?: boolean;
|
||||
schemaType: string;
|
||||
}
|
||||
|
||||
const DiffViewer = React.forwardRef<DiffEditor | null, DiffViewerProps>(
|
||||
(props, ref) => {
|
||||
const { isFixedHeight, schemaType, ...rest } = props;
|
||||
const autoHeight =
|
||||
!isFixedHeight && props.value && props.value.length === 2
|
||||
? Math.max(
|
||||
props.value[0].split(/\r\n|\r|\n/).length + 1,
|
||||
props.value[1].split(/\r\n|\r|\n/).length + 1
|
||||
) * 16
|
||||
: 500;
|
||||
return (
|
||||
<div data-testid="diffviewer">
|
||||
<DiffEditor
|
||||
name="diff-editor"
|
||||
ref={ref}
|
||||
mode={
|
||||
schemaType === SchemaType.JSON || schemaType === SchemaType.AVRO
|
||||
? 'json5'
|
||||
: 'protobuf'
|
||||
}
|
||||
theme="textmate"
|
||||
tabSize={2}
|
||||
width="100%"
|
||||
height={`${autoHeight}px`}
|
||||
showPrintMargin={false}
|
||||
maxLines={Infinity}
|
||||
readOnly
|
||||
wrapEnabled
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
DiffViewer.displayName = 'DiffViewer';
|
||||
|
||||
export default DiffViewer;
|
|
@ -0,0 +1,56 @@
|
|||
import React from 'react';
|
||||
import { render } from 'lib/testHelpers';
|
||||
import DiffViewer from 'components/common/DiffViewer/DiffViewer';
|
||||
import { screen } from '@testing-library/react';
|
||||
|
||||
describe('Editor component', () => {
|
||||
const left = '{\n}';
|
||||
const right = '{\ntest: true\n}';
|
||||
const renderComponent = (props: {
|
||||
leftVersion?: string;
|
||||
rightVersion?: string;
|
||||
isFixedHeight?: boolean;
|
||||
}) => {
|
||||
render(
|
||||
<DiffViewer
|
||||
value={[props.leftVersion ?? '', props.rightVersion ?? '']}
|
||||
name="name"
|
||||
schemaType="JSON"
|
||||
isFixedHeight={props.isFixedHeight}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
it('renders', () => {
|
||||
renderComponent({ leftVersion: left, rightVersion: right });
|
||||
expect(screen.getByTestId('diffviewer')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders with fixed height', () => {
|
||||
renderComponent({
|
||||
leftVersion: left,
|
||||
rightVersion: right,
|
||||
isFixedHeight: true,
|
||||
});
|
||||
const wrapper = screen.getByTestId('diffviewer');
|
||||
expect(wrapper.firstChild).toHaveStyle('height: 500px');
|
||||
});
|
||||
|
||||
it('renders with fixed height with no value', () => {
|
||||
renderComponent({ isFixedHeight: true });
|
||||
const wrapper = screen.getByTestId('diffviewer');
|
||||
expect(wrapper.firstChild).toHaveStyle('height: 500px');
|
||||
});
|
||||
|
||||
it('renders without fixed height with no value', () => {
|
||||
renderComponent({});
|
||||
const wrapper = screen.getByTestId('diffviewer');
|
||||
expect(wrapper.firstChild).toHaveStyle('height: 32px');
|
||||
});
|
||||
|
||||
it('renders without fixed height with one value', () => {
|
||||
renderComponent({ leftVersion: left });
|
||||
const wrapper = screen.getByTestId('diffviewer');
|
||||
expect(wrapper.firstChild).toHaveStyle('height: 48px');
|
||||
});
|
||||
});
|
|
@ -43,6 +43,10 @@ export const clusterSchemaEditPath = (
|
|||
clusterName: ClusterName,
|
||||
subject: SchemaName
|
||||
) => `${clusterSchemasPath(clusterName)}/${subject}/edit`;
|
||||
export const clusterSchemaSchemaDiffPath = (
|
||||
clusterName: ClusterName,
|
||||
subject: SchemaName
|
||||
) => `${clusterSchemaPath(clusterName, subject)}/diff`;
|
||||
|
||||
// Topics
|
||||
export const clusterTopicsPath = (clusterName: ClusterName) =>
|
||||
|
|
Loading…
Add table
Reference in a new issue