* Changes Default value of rentention.bytes * Adds test for TopicForm * Removes bug from Topic/New test * Updates Topic/New test * Adds accessible names for TopicForm * Add id's to Select component
This commit is contained in:
parent
4390923e48
commit
982d29709b
10 changed files with 276 additions and 118 deletions
|
@ -35,7 +35,6 @@ const New: React.FC = () => {
|
|||
clusterName,
|
||||
topicCreation: formatTopicCreation(data),
|
||||
});
|
||||
|
||||
history.push(clusterTopicPath(clusterName, data.name));
|
||||
} catch (error) {
|
||||
const response = await getResponse(error as Response);
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
import React from 'react';
|
||||
import New from 'components/Topics/New/New';
|
||||
import { Route, Router } from 'react-router';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { RootState } from 'redux/interfaces';
|
||||
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 userEvent from '@testing-library/user-event';
|
||||
import { render } from 'lib/testHelpers';
|
||||
|
||||
import { createTopicPayload, createTopicResponsePayload } from './fixtures';
|
||||
|
||||
const mockStore = configureStore();
|
||||
|
||||
const clusterName = 'local';
|
||||
const topicName = 'test-topic';
|
||||
|
||||
const initialState: Partial<RootState> = {};
|
||||
const storeMock = mockStore(initialState);
|
||||
const historyMock = createMemoryHistory();
|
||||
const createTopicAPIPath = `/api/clusters/${clusterName}/topics`;
|
||||
|
||||
const renderComponent = (history = historyMock, store = storeMock) =>
|
||||
render(
|
||||
<Router history={history}>
|
||||
<Route path={clusterTopicNewPath(':clusterName')}>
|
||||
<Provider store={store}>
|
||||
<New />
|
||||
</Provider>
|
||||
</Route>
|
||||
<Route path={clusterTopicPath(':clusterName', ':topicName')}>
|
||||
New topic path
|
||||
</Route>
|
||||
</Router>
|
||||
);
|
||||
|
||||
describe('New', () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.reset();
|
||||
});
|
||||
|
||||
it('validates form', async () => {
|
||||
const mockedHistory = createMemoryHistory({
|
||||
initialEntries: [clusterTopicNewPath(clusterName)],
|
||||
});
|
||||
jest.spyOn(mockedHistory, 'push');
|
||||
renderComponent(mockedHistory);
|
||||
|
||||
await waitFor(() => {
|
||||
userEvent.click(screen.getByText('Send'));
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('name is a required field')).toBeInTheDocument();
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(mockedHistory.push).toBeCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('submits valid form', async () => {
|
||||
const createTopicAPIPathMock = fetchMock.postOnce(
|
||||
createTopicAPIPath,
|
||||
createTopicResponsePayload,
|
||||
{
|
||||
body: createTopicPayload,
|
||||
}
|
||||
);
|
||||
const mockedHistory = createMemoryHistory({
|
||||
initialEntries: [clusterTopicNewPath(clusterName)],
|
||||
});
|
||||
jest.spyOn(mockedHistory, 'push');
|
||||
renderComponent(mockedHistory);
|
||||
|
||||
await waitFor(() => {
|
||||
userEvent.type(screen.getByPlaceholderText('Topic Name'), topicName);
|
||||
userEvent.click(screen.getByText('Send'));
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockedHistory.location.pathname).toBe(
|
||||
clusterTopicPath(clusterName, topicName)
|
||||
)
|
||||
);
|
||||
expect(mockedHistory.push).toBeCalledTimes(1);
|
||||
expect(createTopicAPIPathMock.called()).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
import { CleanUpPolicy, Topic } from 'generated-sources';
|
||||
|
||||
export const createTopicPayload: Record<string, unknown> = {
|
||||
name: 'test-topic',
|
||||
partitions: 1,
|
||||
replicationFactor: 1,
|
||||
configs: {
|
||||
'cleanup.policy': 'delete',
|
||||
'retention.ms': '604800000',
|
||||
'retention.bytes': '-1',
|
||||
'max.message.bytes': '1000012',
|
||||
'min.insync.replicas': '1',
|
||||
},
|
||||
};
|
||||
|
||||
export const createTopicResponsePayload: Topic = {
|
||||
name: 'local',
|
||||
internal: false,
|
||||
partitionCount: 1,
|
||||
replicationFactor: 1,
|
||||
replicas: 1,
|
||||
inSyncReplicas: 1,
|
||||
segmentSize: 0,
|
||||
segmentCount: 0,
|
||||
underReplicatedPartitions: 0,
|
||||
cleanUpPolicy: CleanUpPolicy.DELETE,
|
||||
partitions: [
|
||||
{
|
||||
partition: 0,
|
||||
leader: 1,
|
||||
replicas: [{ broker: 1, leader: false, inSync: true }],
|
||||
offsetMax: 0,
|
||||
offsetMin: 0,
|
||||
},
|
||||
],
|
||||
};
|
|
@ -1,67 +0,0 @@
|
|||
import React from 'react';
|
||||
import New from 'components/Topics/New/New';
|
||||
import { Router } from 'react-router';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { RootState } from 'redux/interfaces';
|
||||
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 userEvent from '@testing-library/user-event';
|
||||
import { render } from 'lib/testHelpers';
|
||||
|
||||
const mockStore = configureStore();
|
||||
|
||||
describe('New', () => {
|
||||
const clusterName = 'local';
|
||||
const topicName = 'test-topic';
|
||||
|
||||
const initialState: Partial<RootState> = {};
|
||||
const storeMock = mockStore(initialState);
|
||||
const historyMock = createMemoryHistory();
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.restore();
|
||||
});
|
||||
|
||||
const setupComponent = (history = historyMock, store = storeMock) => (
|
||||
<Router history={history}>
|
||||
<Provider store={store}>
|
||||
<New />
|
||||
</Provider>
|
||||
</Router>
|
||||
);
|
||||
|
||||
it('validates form', async () => {
|
||||
const mockedHistory = createMemoryHistory();
|
||||
jest.spyOn(mockedHistory, 'push');
|
||||
render(setupComponent(mockedHistory));
|
||||
userEvent.click(screen.getByText('Send'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('name is a required field')).toBeInTheDocument();
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(mockedHistory.push).toBeCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('submits valid form', () => {
|
||||
const mockedHistory = createMemoryHistory({
|
||||
initialEntries: [clusterTopicNewPath(clusterName)],
|
||||
});
|
||||
jest.spyOn(mockedHistory, 'push');
|
||||
render(setupComponent());
|
||||
userEvent.type(screen.getByPlaceholderText('Topic Name'), topicName);
|
||||
userEvent.click(screen.getByText('Send'));
|
||||
waitFor(() => {
|
||||
expect(mockedHistory.location.pathname).toBe(
|
||||
clusterTopicPath(clusterName, topicName)
|
||||
);
|
||||
});
|
||||
waitFor(() => {
|
||||
expect(mockedHistory.push).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -479,6 +479,7 @@ exports[`Filters component matches the snapshot 1`] = `
|
|||
<div>
|
||||
<ul
|
||||
class="c6"
|
||||
id="selectSeekType"
|
||||
role="listbox"
|
||||
>
|
||||
<li
|
||||
|
@ -1122,6 +1123,7 @@ exports[`Filters component when fetching matches the snapshot 1`] = `
|
|||
<div>
|
||||
<ul
|
||||
class="c6"
|
||||
id="selectSeekType"
|
||||
role="listbox"
|
||||
>
|
||||
<li
|
||||
|
|
|
@ -31,7 +31,9 @@ const TimeToRetain: React.FC<Props> = ({ isSubmitting }) => {
|
|||
return (
|
||||
<>
|
||||
<S.Label>
|
||||
<InputLabel>Time to retain data (in ms)</InputLabel>
|
||||
<InputLabel htmlFor="timeToRetain">
|
||||
Time to retain data (in ms)
|
||||
</InputLabel>
|
||||
{valueHint && <span>{valueHint}</span>}
|
||||
</S.Label>
|
||||
<Input
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import { useFormContext, Controller } from 'react-hook-form';
|
||||
import { BYTES_IN_GB } from 'lib/constants';
|
||||
import { NOT_SET, BYTES_IN_GB } from 'lib/constants';
|
||||
import { TopicName, TopicConfigByName } from 'redux/interfaces';
|
||||
import { ErrorMessage } from '@hookform/error-message';
|
||||
import Select, { SelectOption } from 'components/common/Select/Select';
|
||||
|
@ -14,7 +14,7 @@ import CustomParamsContainer from './CustomParams/CustomParamsContainer';
|
|||
import TimeToRetain from './TimeToRetain';
|
||||
import * as S from './TopicForm.styled';
|
||||
|
||||
interface Props {
|
||||
export interface Props {
|
||||
topicName?: TopicName;
|
||||
config?: TopicConfigByName;
|
||||
isEditing?: boolean;
|
||||
|
@ -29,7 +29,7 @@ const CleanupPolicyOptions: Array<SelectOption> = [
|
|||
];
|
||||
|
||||
const RetentionBytesOptions: Array<SelectOption> = [
|
||||
{ value: -1, label: 'Not Set' },
|
||||
{ value: NOT_SET, label: 'Not Set' },
|
||||
{ value: BYTES_IN_GB, label: '1 GB' },
|
||||
{ value: BYTES_IN_GB * 10, label: '10 GB' },
|
||||
{ value: BYTES_IN_GB * 20, label: '20 GB' },
|
||||
|
@ -47,15 +47,15 @@ const TopicForm: React.FC<Props> = ({
|
|||
control,
|
||||
formState: { errors },
|
||||
} = useFormContext();
|
||||
|
||||
return (
|
||||
<StyledForm onSubmit={onSubmit}>
|
||||
<fieldset disabled={isSubmitting}>
|
||||
<fieldset disabled={isEditing}>
|
||||
<S.Column>
|
||||
<S.NameField>
|
||||
<InputLabel>Topic Name *</InputLabel>
|
||||
<InputLabel htmlFor="topicFormName">Topic Name *</InputLabel>
|
||||
<Input
|
||||
id="topicFormName"
|
||||
name="name"
|
||||
placeholder="Topic Name"
|
||||
defaultValue={topicName}
|
||||
|
@ -69,8 +69,11 @@ const TopicForm: React.FC<Props> = ({
|
|||
{!isEditing && (
|
||||
<S.Column>
|
||||
<div>
|
||||
<InputLabel>Number of partitions *</InputLabel>
|
||||
<InputLabel htmlFor="topicFormNumberOfPartitions">
|
||||
Number of partitions *
|
||||
</InputLabel>
|
||||
<Input
|
||||
id="topicFormNumberOfPartitions"
|
||||
type="number"
|
||||
placeholder="Number of partitions"
|
||||
min="1"
|
||||
|
@ -82,8 +85,11 @@ const TopicForm: React.FC<Props> = ({
|
|||
</FormError>
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel>Replication Factor *</InputLabel>
|
||||
<InputLabel htmlFor="topicFormReplicationFactor">
|
||||
Replication Factor *
|
||||
</InputLabel>
|
||||
<Input
|
||||
id="topicFormReplicationFactor"
|
||||
type="number"
|
||||
placeholder="Replication Factor"
|
||||
min="1"
|
||||
|
@ -100,8 +106,11 @@ const TopicForm: React.FC<Props> = ({
|
|||
|
||||
<S.Column>
|
||||
<div>
|
||||
<InputLabel>Min In Sync Replicas *</InputLabel>
|
||||
<InputLabel htmlFor="topicFormMinInSyncReplicas">
|
||||
Min In Sync Replicas *
|
||||
</InputLabel>
|
||||
<Input
|
||||
id="topicFormMinInSyncReplicas"
|
||||
type="number"
|
||||
placeholder="Min In Sync Replicas"
|
||||
min="1"
|
||||
|
@ -113,13 +122,20 @@ const TopicForm: React.FC<Props> = ({
|
|||
</FormError>
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel>Cleanup policy</InputLabel>
|
||||
<InputLabel
|
||||
id="topicFormCleanupPolicyLabel"
|
||||
htmlFor="topicFormCleanupPolicy"
|
||||
>
|
||||
Cleanup policy
|
||||
</InputLabel>
|
||||
<Controller
|
||||
defaultValue={CleanupPolicyOptions[0].value}
|
||||
control={control}
|
||||
name="cleanupPolicy"
|
||||
render={({ field: { name, onChange } }) => (
|
||||
<Select
|
||||
id="topicFormCleanupPolicy"
|
||||
aria-labelledby="topicFormCleanupPolicyLabel"
|
||||
name={name}
|
||||
value={CleanupPolicyOptions[0].value}
|
||||
onChange={onChange}
|
||||
|
@ -131,48 +147,56 @@ const TopicForm: React.FC<Props> = ({
|
|||
</div>
|
||||
</S.Column>
|
||||
|
||||
<div>
|
||||
<S.Column>
|
||||
<div>
|
||||
<TimeToRetain isSubmitting={isSubmitting} />
|
||||
</div>
|
||||
</S.Column>
|
||||
<S.Column>
|
||||
<div>
|
||||
<InputLabel>Max size on disk in GB</InputLabel>
|
||||
<Controller
|
||||
control={control}
|
||||
name="retentionBytes"
|
||||
defaultValue={0}
|
||||
render={({ field: { name, onChange } }) => (
|
||||
<Select
|
||||
name={name}
|
||||
value={RetentionBytesOptions[0].value}
|
||||
onChange={onChange}
|
||||
minWidth="100%"
|
||||
options={RetentionBytesOptions}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<S.Column>
|
||||
<div>
|
||||
<TimeToRetain isSubmitting={isSubmitting} />
|
||||
</div>
|
||||
</S.Column>
|
||||
|
||||
<div>
|
||||
<InputLabel>Maximum message size in bytes *</InputLabel>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
defaultValue="1000012"
|
||||
name="maxMessageBytes"
|
||||
/>
|
||||
<FormError>
|
||||
<ErrorMessage errors={errors} name="maxMessageBytes" />
|
||||
</FormError>
|
||||
</div>
|
||||
</S.Column>
|
||||
</div>
|
||||
<S.Column>
|
||||
<div>
|
||||
<InputLabel
|
||||
id="topicFormRetentionBytesLabel"
|
||||
htmlFor="topicFormRetentionBytes"
|
||||
>
|
||||
Max size on disk in GB
|
||||
</InputLabel>
|
||||
<Controller
|
||||
control={control}
|
||||
name="retentionBytes"
|
||||
defaultValue={RetentionBytesOptions[0].value}
|
||||
render={({ field: { name, onChange } }) => (
|
||||
<Select
|
||||
id="topicFormRetentionBytes"
|
||||
aria-labelledby="topicFormRetentionBytesLabel"
|
||||
name={name}
|
||||
value={RetentionBytesOptions[0].value}
|
||||
onChange={onChange}
|
||||
minWidth="100%"
|
||||
options={RetentionBytesOptions}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel htmlFor="topicFormMaxMessageBytes">
|
||||
Maximum message size in bytes *
|
||||
</InputLabel>
|
||||
<Input
|
||||
id="topicFormMaxMessageBytes"
|
||||
type="number"
|
||||
min="1"
|
||||
defaultValue="1000012"
|
||||
name="maxMessageBytes"
|
||||
/>
|
||||
<FormError>
|
||||
<ErrorMessage errors={errors} name="maxMessageBytes" />
|
||||
</FormError>
|
||||
</div>
|
||||
</S.Column>
|
||||
|
||||
<S.CustomParamsHeading>Custom parameters</S.CustomParamsHeading>
|
||||
|
||||
<CustomParamsContainer isSubmitting={isSubmitting} config={config} />
|
||||
|
||||
<Button type="submit" buttonType="primary" buttonSize="L">
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
import React from 'react';
|
||||
import { render } from 'lib/testHelpers';
|
||||
import { screen } from '@testing-library/dom';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import TopicForm, { Props } from 'components/Topics/shared/Form/TopicForm';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
const isSubmitting = false;
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
const renderComponent = (props: Props = { isSubmitting, onSubmit }) => {
|
||||
const Wrapper: React.FC = ({ children }) => {
|
||||
const methods = useForm();
|
||||
return <FormProvider {...methods}>{children}</FormProvider>;
|
||||
};
|
||||
|
||||
return render(
|
||||
<Wrapper>
|
||||
<TopicForm {...props} />
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const expectByRoleAndNameToBeInDocument = (
|
||||
role: string,
|
||||
accessibleName: string
|
||||
) => {
|
||||
expect(screen.getByRole(role, { name: accessibleName })).toBeInTheDocument();
|
||||
};
|
||||
|
||||
describe('TopicForm', () => {
|
||||
it('renders', () => {
|
||||
renderComponent();
|
||||
|
||||
expectByRoleAndNameToBeInDocument('textbox', 'Topic Name *');
|
||||
|
||||
expectByRoleAndNameToBeInDocument('spinbutton', 'Number of partitions *');
|
||||
expectByRoleAndNameToBeInDocument('spinbutton', 'Replication Factor *');
|
||||
|
||||
expectByRoleAndNameToBeInDocument('spinbutton', 'Min In Sync Replicas *');
|
||||
expectByRoleAndNameToBeInDocument('listbox', 'Cleanup policy');
|
||||
|
||||
expectByRoleAndNameToBeInDocument(
|
||||
'spinbutton',
|
||||
'Time to retain data (in ms)'
|
||||
);
|
||||
expectByRoleAndNameToBeInDocument('button', '12h');
|
||||
expectByRoleAndNameToBeInDocument('button', '2d');
|
||||
expectByRoleAndNameToBeInDocument('button', '7d');
|
||||
expectByRoleAndNameToBeInDocument('button', '4w');
|
||||
|
||||
expectByRoleAndNameToBeInDocument('listbox', 'Max size on disk in GB');
|
||||
expectByRoleAndNameToBeInDocument(
|
||||
'spinbutton',
|
||||
'Maximum message size in bytes *'
|
||||
);
|
||||
|
||||
expectByRoleAndNameToBeInDocument('heading', 'Custom parameters');
|
||||
|
||||
expectByRoleAndNameToBeInDocument('button', 'Send');
|
||||
});
|
||||
|
||||
it('submits', () => {
|
||||
renderComponent({
|
||||
isSubmitting,
|
||||
onSubmit: onSubmit.mockImplementation((e) => e.preventDefault()),
|
||||
});
|
||||
|
||||
userEvent.click(screen.getByRole('button', { name: 'Send' }));
|
||||
expect(onSubmit).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -25,7 +25,6 @@ export interface SelectOption {
|
|||
}
|
||||
|
||||
const Select: React.FC<SelectProps> = ({
|
||||
id,
|
||||
options = [],
|
||||
value,
|
||||
defaultValue,
|
||||
|
|
|
@ -46,6 +46,7 @@ export const MILLISECONDS_IN_WEEK = 604_800_000;
|
|||
export const MILLISECONDS_IN_DAY = 86_400_000;
|
||||
export const MILLISECONDS_IN_SECOND = 1_000;
|
||||
|
||||
export const NOT_SET = -1;
|
||||
export const BYTES_IN_GB = 1_073_741_824;
|
||||
|
||||
export const PER_PAGE = 25;
|
||||
|
|
Loading…
Add table
Reference in a new issue