С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, TopicName,
} from 'redux/interfaces'; } from 'redux/interfaces';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { clusterTopicNewPath } from 'lib/paths'; import { clusterTopicCopyPath, clusterTopicNewPath } from 'lib/paths';
import usePagination from 'lib/hooks/usePagination'; import usePagination from 'lib/hooks/usePagination';
import ClusterContext from 'components/contexts/ClusterContext'; import ClusterContext from 'components/contexts/ClusterContext';
import PageLoader from 'components/common/PageLoader/PageLoader'; 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(() => { const handleSwitch = React.useCallback(() => {
setShowInternal(!showInternal); setShowInternal(!showInternal);
history.push(`${pathname}?page=1&perPage=${perPage || PER_PAGE}`); history.push(`${pathname}?page=1&perPage=${perPage || PER_PAGE}`);
@ -295,6 +310,20 @@ const List: React.FC<TopicsListProps> = ({
> >
Delete selected topics Delete selected topics
</Button> </Button>
{tableState.selectedCount === 1 && (
<Button
buttonSize="M"
buttonType="secondary"
isLink
to={{
pathname: clusterTopicCopyPath(clusterName),
search: `?${getSelectedTopic()}`,
}}
>
Copy selected topic
</Button>
)}
<Button <Button
buttonSize="M" buttonSize="M"
buttonType="secondary" buttonType="secondary"

View file

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

View file

@ -7,7 +7,11 @@ import { Provider } from 'react-redux';
import { screen, waitFor } from '@testing-library/react'; import { screen, waitFor } from '@testing-library/react';
import { createMemoryHistory } from 'history'; import { createMemoryHistory } from 'history';
import fetchMock from 'fetch-mock-jest'; 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 userEvent from '@testing-library/user-event';
import { render } from 'lib/testHelpers'; import { render } from 'lib/testHelpers';
@ -31,6 +35,11 @@ const renderComponent = (history = historyMock, store = storeMock) =>
<New /> <New />
</Provider> </Provider>
</Route> </Route>
<Route path={clusterTopicCopyPath(':clusterName')}>
<Provider store={store}>
<New />
</Provider>
</Route>
<Route path={clusterTopicPath(':clusterName', ':topicName')}> <Route path={clusterTopicPath(':clusterName', ':topicName')}>
New topic path New topic path
</Route> </Route>
@ -42,6 +51,32 @@ describe('New', () => {
fetchMock.reset(); 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 () => { it('validates form', async () => {
const mockedHistory = createMemoryHistory({ const mockedHistory = createMemoryHistory({
initialEntries: [clusterTopicNewPath(clusterName)], initialEntries: [clusterTopicNewPath(clusterName)],

View file

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

View file

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

View file

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