Сlone topic functionality (FE) (#1825)

* Added Copy button in list of topics

* Added copy topic functionality

* Whitespaces removed

* Removed extra string wrapper

* Copy component removed, routing fixed, tests fixed

* Ternary and returning null removed

* Dublicated code refactored

* Added tests for ternary header

* Added enum for the form fields

Co-authored-by: k.morozov <k.morozov@ffin.ru>
Co-authored-by: Roman Zabaluev <rzabaluev@provectus.com>
This commit is contained in:
Kirill Morozov 2022-04-22 16:51:00 +03:00 committed by GitHub
parent 29ee6b1517
commit deddf09ed4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 114 additions and 8 deletions

View file

@ -6,7 +6,7 @@ import {
TopicName,
} from 'redux/interfaces';
import { useParams } from 'react-router-dom';
import { clusterTopicNewPath } from 'lib/paths';
import { clusterTopicCopyPath, clusterTopicNewPath } from 'lib/paths';
import usePagination from 'lib/hooks/usePagination';
import ClusterContext from 'components/contexts/ClusterContext';
import PageLoader from 'components/common/PageLoader/PageLoader';
@ -125,6 +125,21 @@ const List: React.FC<TopicsListProps> = ({
}
);
const getSelectedTopic = (): string => {
const name = Array.from(tableState.selectedIds)[0];
const selectedTopic =
tableState.data.find(
(topic: TopicWithDetailedInfo) => topic.name === name
) || {};
return Object.keys(selectedTopic)
.map((x: string) => {
const value = selectedTopic[x as keyof typeof selectedTopic];
return value && x !== 'partitions' ? `${x}=${value}` : null;
})
.join('&');
};
const handleSwitch = React.useCallback(() => {
setShowInternal(!showInternal);
history.push(`${pathname}?page=1&perPage=${perPage || PER_PAGE}`);
@ -295,6 +310,20 @@ const List: React.FC<TopicsListProps> = ({
>
Delete selected topics
</Button>
{tableState.selectedCount === 1 && (
<Button
buttonSize="M"
buttonType="secondary"
isLink
to={{
pathname: clusterTopicCopyPath(clusterName),
search: `?${getSelectedTopic()}`,
}}
>
Copy selected topic
</Button>
)}
<Button
buttonSize="M"
buttonType="secondary"

View file

@ -10,7 +10,7 @@ import {
} from 'redux/actions';
import { useDispatch } from 'react-redux';
import { getResponse } from 'lib/errorHandling';
import { useHistory, useParams } from 'react-router';
import { useHistory, useLocation, useParams } from 'react-router';
import { yupResolver } from '@hookform/resolvers/yup';
import { topicFormValidationSchema } from 'lib/yupExtended';
import PageHeading from 'components/common/PageHeading/PageHeading';
@ -19,6 +19,14 @@ interface RouterParams {
clusterName: ClusterName;
}
enum Filters {
NAME = 'name',
PARTITION_COUNT = 'partitionCount',
REPLICATION_FACTOR = 'replicationFactor',
INSYNC_REPLICAS = 'inSyncReplicas',
CLEANUP_POLICY = 'Delete',
}
const New: React.FC = () => {
const methods = useForm<TopicFormData>({
mode: 'all',
@ -29,6 +37,15 @@ const New: React.FC = () => {
const history = useHistory();
const dispatch = useDispatch();
const { search } = useLocation();
const params = new URLSearchParams(search);
const name = params.get(Filters.NAME) || '';
const partitionCount = params.get(Filters.PARTITION_COUNT) || 1;
const replicationFactor = params.get(Filters.REPLICATION_FACTOR) || 1;
const inSyncReplicas = params.get(Filters.INSYNC_REPLICAS) || 1;
const cleanUpPolicy = params.get(Filters.CLEANUP_POLICY) || 'Delete';
const onSubmit = async (data: TopicFormData) => {
try {
await topicsApiClient.createTopic({
@ -50,9 +67,14 @@ const New: React.FC = () => {
return (
<>
<PageHeading text="Create new Topic" />
<PageHeading text={search ? 'Copy Topic' : 'Create new Topic'} />
<FormProvider {...methods}>
<TopicForm
topicName={name}
cleanUpPolicy={cleanUpPolicy}
partitionCount={Number(partitionCount)}
replicationFactor={Number(replicationFactor)}
inSyncReplicas={Number(inSyncReplicas)}
isSubmitting={methods.formState.isSubmitting}
onSubmit={methods.handleSubmit(onSubmit)}
/>

View file

@ -7,7 +7,11 @@ import { Provider } from 'react-redux';
import { screen, waitFor } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import fetchMock from 'fetch-mock-jest';
import { clusterTopicNewPath, clusterTopicPath } from 'lib/paths';
import {
clusterTopicCopyPath,
clusterTopicNewPath,
clusterTopicPath,
} from 'lib/paths';
import userEvent from '@testing-library/user-event';
import { render } from 'lib/testHelpers';
@ -31,6 +35,11 @@ const renderComponent = (history = historyMock, store = storeMock) =>
<New />
</Provider>
</Route>
<Route path={clusterTopicCopyPath(':clusterName')}>
<Provider store={store}>
<New />
</Provider>
</Route>
<Route path={clusterTopicPath(':clusterName', ':topicName')}>
New topic path
</Route>
@ -42,6 +51,32 @@ describe('New', () => {
fetchMock.reset();
});
it('checks header for create new', async () => {
const mockedHistory = createMemoryHistory({
initialEntries: [clusterTopicNewPath(clusterName)],
});
renderComponent(mockedHistory);
expect(
screen.getByRole('heading', { name: 'Create new Topic' })
).toHaveTextContent('Create new Topic');
});
it('checks header for copy', async () => {
const mockedHistory = createMemoryHistory({
initialEntries: [
{
pathname: clusterTopicCopyPath(clusterName),
search: `?name=test`,
},
],
});
renderComponent(mockedHistory);
expect(
screen.getByRole('heading', { name: 'Copy Topic' })
).toHaveTextContent('Copy Topic');
});
it('validates form', async () => {
const mockedHistory = createMemoryHistory({
initialEntries: [clusterTopicNewPath(clusterName)],

View file

@ -1,6 +1,7 @@
import React from 'react';
import { Switch } from 'react-router-dom';
import {
clusterTopicCopyPath,
clusterTopicNewPath,
clusterTopicPath,
clusterTopicsPath,
@ -23,6 +24,11 @@ const Topics: React.FC = () => (
path={clusterTopicNewPath(':clusterName')}
component={New}
/>
<BreadcrumbRoute
exact
path={clusterTopicCopyPath(':clusterName')}
component={New}
/>
<BreadcrumbRoute
path={clusterTopicPath(':clusterName', ':topicName')}
component={TopicContainer}

View file

@ -16,6 +16,10 @@ import * as S from './TopicForm.styled';
export interface Props {
topicName?: TopicName;
partitionCount?: number;
replicationFactor?: number;
inSyncReplicas?: number;
cleanUpPolicy?: string;
isEditing?: boolean;
isSubmitting: boolean;
onSubmit: (e: React.BaseSyntheticEvent) => Promise<void>;
@ -40,11 +44,19 @@ const TopicForm: React.FC<Props> = ({
isEditing,
isSubmitting,
onSubmit,
partitionCount,
replicationFactor,
inSyncReplicas,
cleanUpPolicy,
}) => {
const {
control,
formState: { errors },
} = useFormContext();
const getCleanUpPolicy =
CleanupPolicyOptions.find((option: SelectOption) => {
return option.value === cleanUpPolicy?.toLowerCase();
})?.value || CleanupPolicyOptions[0].value;
return (
<StyledForm onSubmit={onSubmit}>
<fieldset disabled={isSubmitting}>
@ -75,7 +87,7 @@ const TopicForm: React.FC<Props> = ({
type="number"
placeholder="Number of partitions"
min="1"
defaultValue="1"
defaultValue={partitionCount}
name="partitions"
/>
<FormError>
@ -91,7 +103,7 @@ const TopicForm: React.FC<Props> = ({
type="number"
placeholder="Replication Factor"
min="1"
defaultValue="1"
defaultValue={replicationFactor}
name="replicationFactor"
/>
<FormError>
@ -112,7 +124,7 @@ const TopicForm: React.FC<Props> = ({
type="number"
placeholder="Min In Sync Replicas"
min="1"
defaultValue="1"
defaultValue={inSyncReplicas}
name="minInsyncReplicas"
/>
<FormError>
@ -135,7 +147,7 @@ const TopicForm: React.FC<Props> = ({
id="topicFormCleanupPolicy"
aria-labelledby="topicFormCleanupPolicyLabel"
name={name}
value={CleanupPolicyOptions[0].value}
value={getCleanUpPolicy}
onChange={onChange}
minWidth="250px"
options={CleanupPolicyOptions}

View file

@ -53,6 +53,8 @@ export const clusterTopicsPath = (clusterName: ClusterName) =>
`${clusterPath(clusterName)}/topics`;
export const clusterTopicNewPath = (clusterName: ClusterName) =>
`${clusterPath(clusterName)}/topics/create-new`;
export const clusterTopicCopyPath = (clusterName: ClusterName) =>
`${clusterPath(clusterName)}/topics/copy`;
export const clusterTopicPath = (
clusterName: ClusterName,
topicName: TopicName