С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:
parent
29ee6b1517
commit
deddf09ed4
6 changed files with 114 additions and 8 deletions
|
@ -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"
|
||||||
|
|
|
@ -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)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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)],
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Reference in a new issue