С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,
|
||||
} 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"
|
||||
|
|
|
@ -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)}
|
||||
/>
|
||||
|
|
|
@ -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)],
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue