[Fixes #1534] No originally selected custom parameters in topic's edit tab (#1538)

- Fixes No originally selected custom parameters in topic's edit tab #1534
- Adds test case for CustomParams to verify fix
- Updates CustomParamsField to use predefined value
- Renames INDEX_PREFIX to TOPIC_CUSTOM_PARAMS_PREFIX and moves it to constants file
- Removes unused configs from Topic/Edit component
- Rewrites DangerZone styled's
- Rewrites DangerZone tests
- Adds margin to DangerZone to match TopicForm width
- Adds simple Topic/Edit test
- Changes sonar-project.properties file's sonar.exclusions to correctly ignore paths

Signed-off-by: Roman Zabaluev <rzabaluev@provectus.com>

Co-authored-by: Roman Zabaluev <rzabaluev@provectus.com>
This commit is contained in:
Damir Abdulganiev 2022-02-03 13:29:02 +03:00 committed by GitHub
parent cf45ee0198
commit ced74ac550
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 789 additions and 499 deletions

View file

@ -19,7 +19,7 @@ jobs:
- name: compose app
id: step_five
run: |
docker-compose -f ./docker/kafka-ui.yaml up -d
docker-compose -f ./documentation/compose/kafka-ui.yaml up -d
- name: Set up JDK 1.13
uses: actions/setup-java@v1
with:

View file

@ -39,7 +39,7 @@ jobs:
id: compose_app
# use the following command until #819 will be fixed
run: |
docker-compose -f ./docker/kafka-ui-connectors.yaml up -d
docker-compose -f ./documentation/compose/kafka-ui-connectors.yaml up -d
- name: e2e run
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any

View file

@ -2,7 +2,7 @@ sonar.projectKey=provectus_kafka-ui_frontend
sonar.organization=provectus
sonar.sources=.
sonar.exclusions="**/__test?__/**,src/setupWorker.ts,src/setupTests.ts,**/fixtures.ts,src/lib/testHelpers.tsx"
sonar.exclusions=**/__test?__/**,src/setupWorker.ts,src/setupTests.ts,**/fixtures.ts,src/lib/testHelpers.tsx
sonar.typescript.lcov.reportPaths=./coverage/lcov.info
sonar.testExecutionReportPaths=./test-report.xml

View file

@ -1,12 +1,11 @@
import styled from 'styled-components';
export const DangerZoneWrapperStyled = styled.div`
margin-top: 16px;
export const Wrapper = styled.div`
margin: 16px;
padding: 8px 16px;
border: 1px solid ${({ theme }) => theme.dangerZone.borderColor};
box-sizing: border-box;
border-radius: 8px;
margin-bottom: 16px;
& > div {
display: flex;
@ -15,13 +14,13 @@ export const DangerZoneWrapperStyled = styled.div`
}
`;
export const DangerZoneTitleStyled = styled.h1`
export const Title = styled.h1`
color: ${({ theme }) => theme.dangerZone.color};
font-size: 20px;
padding-bottom: 16px;
`;
export const DagerZoneFormStyled = styled.form`
export const Form = styled.form`
display: flex;
align-items: flex-end;
gap: 16px;

View file

@ -7,11 +7,7 @@ import { InputLabel } from 'components/common/Input/InputLabel.styled';
import React from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import {
DagerZoneFormStyled,
DangerZoneTitleStyled,
DangerZoneWrapperStyled,
} from './DangerZone.styled';
import * as S from './DangerZone.styled';
export interface Props {
clusterName: string;
@ -109,12 +105,13 @@ const DangerZone: React.FC<Props> = ({
);
};
return (
<DangerZoneWrapperStyled>
<DangerZoneTitleStyled>Danger Zone</DangerZoneTitleStyled>
<S.Wrapper>
<S.Title>Danger Zone</S.Title>
<div>
<FormProvider {...partitionsMethods}>
<DagerZoneFormStyled
<S.Form
onSubmit={partitionsMethods.handleSubmit(validatePartitions)}
aria-label="Edit number of partitions"
>
<div>
<InputLabel htmlFor="partitions">
@ -137,12 +134,11 @@ const DangerZone: React.FC<Props> = ({
buttonSize="M"
type="submit"
disabled={!partitionsMethods.formState.isDirty}
data-testid="partitionsSubmit"
>
Submit
</Button>
</div>
</DagerZoneFormStyled>
</S.Form>
</FormProvider>
<FormError>
<ErrorMessage
@ -160,10 +156,11 @@ const DangerZone: React.FC<Props> = ({
</ConfirmationModal>
<FormProvider {...replicationFactorMethods}>
<DagerZoneFormStyled
<S.Form
onSubmit={replicationFactorMethods.handleSubmit(
validateReplicationFactor
)}
aria-label="Edit replication factor"
>
<div>
<InputLabel htmlFor="replicationFactor">
@ -185,12 +182,11 @@ const DangerZone: React.FC<Props> = ({
buttonSize="M"
type="submit"
disabled={!replicationFactorMethods.formState.isDirty}
data-testid="replicationFactorSubmit"
>
Submit
</Button>
</div>
</DagerZoneFormStyled>
</S.Form>
</FormProvider>
<FormError>
@ -207,7 +203,7 @@ const DangerZone: React.FC<Props> = ({
Are you sure you want to update the replication factor?
</ConfirmationModal>
</div>
</DangerZoneWrapperStyled>
</S.Wrapper>
);
};

View file

@ -7,21 +7,19 @@ import {
TopicWithDetailedInfo,
TopicFormData,
} from 'redux/interfaces';
import { TopicConfig } from 'generated-sources';
import { useForm, FormProvider } from 'react-hook-form';
import { camelCase } from 'lodash';
import TopicForm from 'components/Topics/shared/Form/TopicForm';
import { clusterTopicPath } from 'lib/paths';
import { useHistory } from 'react-router';
import { yupResolver } from '@hookform/resolvers/yup';
import { topicFormValidationSchema } from 'lib/yupExtended';
import { TOPIC_CUSTOM_PARAMS } from 'lib/constants';
import { TOPIC_CUSTOM_PARAMS_PREFIX, TOPIC_CUSTOM_PARAMS } from 'lib/constants';
import styled from 'styled-components';
import PageHeading from 'components/common/PageHeading/PageHeading';
import DangerZoneContainer from './DangerZone/DangerZoneContainer';
interface Props {
export interface Props {
clusterName: ClusterName;
topicName: TopicName;
topic?: TopicWithDetailedInfo;
@ -64,27 +62,18 @@ const topicParams = (topic: TopicWithDetailedInfo | undefined) => {
const { name, replicationFactor } = topic;
const configs = topic.config?.reduce(
(result: { [key: string]: TopicConfig['value'] }, param) => ({
...result,
[camelCase(param.name)]: param.value || param.defaultValue,
}),
{}
);
return {
...DEFAULTS,
name,
partitions: topic.partitionCount || DEFAULTS.partitions,
replicationFactor,
customParams: topic.config
[TOPIC_CUSTOM_PARAMS_PREFIX]: topic.config
?.filter(
(el) =>
el.value !== el.defaultValue &&
Object.keys(TOPIC_CUSTOM_PARAMS).includes(el.name)
)
.map((el) => ({ name: el.name, value: el.value })),
...configs,
};
};
@ -99,8 +88,10 @@ const Edit: React.FC<Props> = ({
fetchTopicConfig,
updateTopic,
}) => {
const defaultValues = topicParams(topic);
const defaultValues = React.useMemo(
() => topicParams(topic),
[topicParams, topic]
);
const methods = useForm<TopicFormData>({
defaultValues,
resolver: yupResolver(topicFormValidationSchema),
@ -146,18 +137,15 @@ const Edit: React.FC<Props> = ({
<>
<PageHeading text={`Edit ${topicName}`} />
<EditWrapperStyled>
<div>
<div>
<FormProvider {...methods}>
<TopicForm
topicName={topicName}
config={config}
isSubmitting={isSubmitting}
isEditing
onSubmit={methods.handleSubmit(onSubmit)}
/>
</FormProvider>
</div>
{topic && (
<DangerZoneContainer
defaultPartitions={defaultValues.partitions}

View file

@ -0,0 +1,116 @@
import React from 'react';
import DangerZone, {
Props,
} from 'components/Topics/Topic/Edit/DangerZone/DangerZone';
import { screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render } from 'lib/testHelpers';
import { topicName, clusterName } from './fixtures';
const renderComponent = (props?: Partial<Props>) =>
render(
<DangerZone
clusterName={clusterName}
topicName={topicName}
defaultPartitions={3}
defaultReplicationFactor={3}
partitionsCountIncreased={false}
replicationFactorUpdated={false}
updateTopicPartitionsCount={jest.fn()}
updateTopicReplicationFactor={jest.fn()}
{...props}
/>
);
const clickOnDialogSubmitButton = () => {
userEvent.click(
within(screen.getByRole('dialog')).getByRole('button', {
name: 'Submit',
})
);
};
describe('DangerZone', () => {
it('renders', () => {
renderComponent();
const numberOfPartitionsEditForm = screen.getByRole('form', {
name: 'Edit number of partitions',
});
expect(numberOfPartitionsEditForm).toBeInTheDocument();
expect(
within(numberOfPartitionsEditForm).getByRole('spinbutton', {
name: 'Number of partitions *',
})
).toBeInTheDocument();
expect(
within(numberOfPartitionsEditForm).getByRole('button', { name: 'Submit' })
).toBeInTheDocument();
const replicationFactorEditForm = screen.getByRole('form', {
name: 'Edit replication factor',
});
expect(replicationFactorEditForm).toBeInTheDocument();
expect(
within(replicationFactorEditForm).getByRole('spinbutton', {
name: 'Replication Factor *',
})
).toBeInTheDocument();
expect(
within(replicationFactorEditForm).getByRole('button', { name: 'Submit' })
).toBeInTheDocument();
});
it('calls updateTopicPartitionsCount', async () => {
const mockUpdateTopicPartitionsCount = jest.fn();
renderComponent({
updateTopicPartitionsCount: mockUpdateTopicPartitionsCount,
});
const numberOfPartitionsEditForm = screen.getByRole('form', {
name: 'Edit number of partitions',
});
userEvent.type(
within(numberOfPartitionsEditForm).getByRole('spinbutton'),
'4'
);
userEvent.click(within(numberOfPartitionsEditForm).getByRole('button'));
await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument());
await waitFor(() => clickOnDialogSubmitButton());
expect(mockUpdateTopicPartitionsCount).toHaveBeenCalledTimes(1);
});
it('calls updateTopicReplicationFactor', async () => {
const mockUpdateTopicReplicationFactor = jest.fn();
renderComponent({
updateTopicReplicationFactor: mockUpdateTopicReplicationFactor,
});
const replicationFactorEditForm = screen.getByRole('form', {
name: 'Edit replication factor',
});
expect(
within(replicationFactorEditForm).getByRole('spinbutton', {
name: 'Replication Factor *',
})
).toBeInTheDocument();
expect(
within(replicationFactorEditForm).getByRole('button', { name: 'Submit' })
).toBeInTheDocument();
userEvent.type(
within(replicationFactorEditForm).getByRole('spinbutton'),
'4'
);
userEvent.click(within(replicationFactorEditForm).getByRole('button'));
await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument());
await waitFor(() => clickOnDialogSubmitButton());
await waitFor(() => {
expect(mockUpdateTopicReplicationFactor).toHaveBeenCalledTimes(1);
});
});
});

View file

@ -0,0 +1,34 @@
import React from 'react';
import Edit, { Props } from 'components/Topics/Topic/Edit/Edit';
import { screen } from '@testing-library/react';
import { render } from 'lib/testHelpers';
import { topicName, clusterName, topicWithInfo } from './fixtures';
const renderComponent = (props?: Partial<Props>) =>
render(
<Edit
clusterName={clusterName}
topicName={topicName}
topic={topicWithInfo}
isFetched
isTopicUpdated={false}
fetchTopicConfig={jest.fn()}
updateTopic={jest.fn()}
updateTopicPartitionsCount={jest.fn()}
{...props}
/>
);
describe('DangerZone', () => {
it('renders', () => {
renderComponent();
expect(
screen.getByRole('heading', { name: `Edit ${topicName}` })
).toBeInTheDocument();
expect(
screen.getByRole('heading', { name: `Danger Zone` })
).toBeInTheDocument();
});
});

View file

@ -0,0 +1,553 @@
import { CleanUpPolicy, ConfigSource, TopicConfig } from 'generated-sources';
import { TopicWithDetailedInfo } from 'redux/interfaces/topic';
export const clusterName = 'testCluster';
export const topicName = 'testTopic';
export const config: TopicConfig[] = [
{
name: 'compression.type',
value: 'producer',
defaultValue: 'producer',
source: ConfigSource.DYNAMIC_TOPIC_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [
{
name: 'compression.type',
value: 'producer',
source: ConfigSource.DYNAMIC_TOPIC_CONFIG,
},
{
name: 'compression.type',
value: 'producer',
source: ConfigSource.DEFAULT_CONFIG,
},
],
},
{
name: 'confluent.value.schema.validation',
value: 'false',
source: ConfigSource.DEFAULT_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [],
},
{
name: 'leader.replication.throttled.replicas',
value: '',
defaultValue: '',
source: ConfigSource.DEFAULT_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [],
},
{
name: 'confluent.key.subject.name.strategy',
value: 'io.confluent.kafka.serializers.subject.TopicNameStrategy',
source: ConfigSource.DEFAULT_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [],
},
{
name: 'message.downconversion.enable',
value: 'true',
defaultValue: 'true',
source: ConfigSource.DEFAULT_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [
{
name: 'log.message.downconversion.enable',
value: 'true',
source: ConfigSource.DEFAULT_CONFIG,
},
],
},
{
name: 'min.insync.replicas',
value: '1',
defaultValue: '1',
source: ConfigSource.DYNAMIC_TOPIC_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [
{
name: 'min.insync.replicas',
value: '1',
source: ConfigSource.DYNAMIC_TOPIC_CONFIG,
},
{
name: 'min.insync.replicas',
value: '1',
source: ConfigSource.DEFAULT_CONFIG,
},
],
},
{
name: 'segment.jitter.ms',
value: '0',
defaultValue: '0',
source: ConfigSource.DEFAULT_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [],
},
{
name: 'cleanup.policy',
value: 'delete',
defaultValue: 'delete',
source: ConfigSource.DYNAMIC_TOPIC_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [
{
name: 'cleanup.policy',
value: 'delete',
source: ConfigSource.DYNAMIC_TOPIC_CONFIG,
},
{
name: 'log.cleanup.policy',
value: 'delete',
source: ConfigSource.DEFAULT_CONFIG,
},
],
},
{
name: 'flush.ms',
value: '9223372036854775807',
defaultValue: '9223372036854775807',
source: ConfigSource.DEFAULT_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [],
},
{
name: 'confluent.tier.local.hotset.ms',
value: '86400000',
source: ConfigSource.DEFAULT_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [
{
name: 'confluent.tier.local.hotset.ms',
value: '86400000',
source: ConfigSource.DEFAULT_CONFIG,
},
],
},
{
name: 'follower.replication.throttled.replicas',
value: '',
defaultValue: '',
source: ConfigSource.DEFAULT_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [],
},
{
name: 'confluent.tier.local.hotset.bytes',
value: '-1',
source: ConfigSource.DEFAULT_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [
{
name: 'confluent.tier.local.hotset.bytes',
value: '-1',
source: ConfigSource.DEFAULT_CONFIG,
},
],
},
{
name: 'confluent.value.subject.name.strategy',
value: 'io.confluent.kafka.serializers.subject.TopicNameStrategy',
source: ConfigSource.DEFAULT_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [],
},
{
name: 'segment.bytes',
value: '1073741824',
defaultValue: '1073741824',
source: ConfigSource.DEFAULT_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [
{
name: 'log.segment.bytes',
value: '1073741824',
source: ConfigSource.DEFAULT_CONFIG,
},
],
},
{
name: 'retention.ms',
value: '604800000',
defaultValue: '604800000',
source: ConfigSource.DYNAMIC_TOPIC_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [
{
name: 'retention.ms',
value: '604800000',
source: ConfigSource.DYNAMIC_TOPIC_CONFIG,
},
],
},
{
name: 'flush.messages',
value: '9223372036854775807',
defaultValue: '9223372036854775807',
source: ConfigSource.DEFAULT_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [
{
name: 'log.flush.interval.messages',
value: '9223372036854775807',
source: ConfigSource.DEFAULT_CONFIG,
},
],
},
{
name: 'confluent.tier.enable',
value: 'false',
source: ConfigSource.DEFAULT_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [
{
name: 'confluent.tier.enable',
value: 'false',
source: ConfigSource.DEFAULT_CONFIG,
},
],
},
{
name: 'confluent.tier.segment.hotset.roll.min.bytes',
value: '104857600',
source: ConfigSource.DEFAULT_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [
{
name: 'confluent.tier.segment.hotset.roll.min.bytes',
value: '104857600',
source: ConfigSource.DEFAULT_CONFIG,
},
],
},
{
name: 'confluent.segment.speculative.prefetch.enable',
value: 'false',
source: ConfigSource.DEFAULT_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [
{
name: 'confluent.segment.speculative.prefetch.enable',
value: 'false',
source: ConfigSource.DEFAULT_CONFIG,
},
],
},
{
name: 'message.format.version',
value: '2.7-IV2',
defaultValue: '2.7-IV2',
source: ConfigSource.DEFAULT_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [
{
name: 'log.message.format.version',
value: '2.7-IV2',
source: ConfigSource.DEFAULT_CONFIG,
},
],
},
{
name: 'max.compaction.lag.ms',
value: '9223372036854775807',
defaultValue: '9223372036854775807',
source: ConfigSource.DEFAULT_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [
{
name: 'log.cleaner.max.compaction.lag.ms',
value: '9223372036854775807',
source: ConfigSource.DEFAULT_CONFIG,
},
],
},
{
name: 'file.delete.delay.ms',
value: '60000',
defaultValue: '60000',
source: ConfigSource.DEFAULT_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [
{
name: 'log.segment.delete.delay.ms',
value: '60000',
source: ConfigSource.DEFAULT_CONFIG,
},
],
},
{
name: 'max.message.bytes',
value: '1000012',
defaultValue: '1000012',
source: ConfigSource.DYNAMIC_TOPIC_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [
{
name: 'max.message.bytes',
value: '1000012',
source: ConfigSource.DYNAMIC_TOPIC_CONFIG,
},
{
name: 'message.max.bytes',
value: '1048588',
source: ConfigSource.DEFAULT_CONFIG,
},
],
},
{
name: 'min.compaction.lag.ms',
value: '0',
defaultValue: '0',
source: ConfigSource.DEFAULT_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [
{
name: 'log.cleaner.min.compaction.lag.ms',
value: '0',
source: ConfigSource.DEFAULT_CONFIG,
},
],
},
{
name: 'message.timestamp.type',
value: 'CreateTime',
defaultValue: 'CreateTime',
source: ConfigSource.DEFAULT_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [
{
name: 'log.message.timestamp.type',
value: 'CreateTime',
source: ConfigSource.DEFAULT_CONFIG,
},
],
},
{
name: 'preallocate',
value: 'false',
defaultValue: 'false',
source: ConfigSource.DEFAULT_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [
{
name: 'log.preallocate',
value: 'false',
source: ConfigSource.DEFAULT_CONFIG,
},
],
},
{
name: 'confluent.placement.constraints',
value: '',
source: ConfigSource.DEFAULT_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [],
},
{
name: 'min.cleanable.dirty.ratio',
value: '0.5',
defaultValue: '0.5',
source: ConfigSource.DEFAULT_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [
{
name: 'log.cleaner.min.cleanable.ratio',
value: '0.5',
source: ConfigSource.DEFAULT_CONFIG,
},
],
},
{
name: 'index.interval.bytes',
value: '4096',
defaultValue: '4096',
source: ConfigSource.DEFAULT_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [
{
name: 'log.index.interval.bytes',
value: '4096',
source: ConfigSource.DEFAULT_CONFIG,
},
],
},
{
name: 'unclean.leader.election.enable',
value: 'false',
defaultValue: 'false',
source: ConfigSource.DEFAULT_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [
{
name: 'unclean.leader.election.enable',
value: 'false',
source: ConfigSource.DEFAULT_CONFIG,
},
],
},
{
name: 'retention.bytes',
value: '-1',
defaultValue: '-1',
source: ConfigSource.DYNAMIC_TOPIC_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [
{
name: 'retention.bytes',
value: '-1',
source: ConfigSource.DYNAMIC_TOPIC_CONFIG,
},
{
name: 'log.retention.bytes',
value: '-1',
source: ConfigSource.DEFAULT_CONFIG,
},
],
},
{
name: 'delete.retention.ms',
value: '86400001',
defaultValue: '86400000',
source: ConfigSource.DYNAMIC_TOPIC_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [
{
name: 'delete.retention.ms',
value: '86400001',
source: ConfigSource.DYNAMIC_TOPIC_CONFIG,
},
{
name: 'log.cleaner.delete.retention.ms',
value: '86400000',
source: ConfigSource.DEFAULT_CONFIG,
},
],
},
{
name: 'confluent.prefer.tier.fetch.ms',
value: '-1',
source: ConfigSource.DEFAULT_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [
{
name: 'confluent.prefer.tier.fetch.ms',
value: '-1',
source: ConfigSource.DEFAULT_CONFIG,
},
],
},
{
name: 'confluent.key.schema.validation',
value: 'false',
source: ConfigSource.DEFAULT_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [],
},
{
name: 'segment.ms',
value: '604800000',
defaultValue: '604800000',
source: ConfigSource.DEFAULT_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [],
},
{
name: 'message.timestamp.difference.max.ms',
value: '9223372036854775807',
defaultValue: '9223372036854775807',
source: ConfigSource.DEFAULT_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [
{
name: 'log.message.timestamp.difference.max.ms',
value: '9223372036854775807',
source: ConfigSource.DEFAULT_CONFIG,
},
],
},
{
name: 'segment.index.bytes',
value: '10485760',
defaultValue: '10485760',
source: ConfigSource.DEFAULT_CONFIG,
isSensitive: false,
isReadOnly: false,
synonyms: [
{
name: 'log.index.size.max.bytes',
value: '10485760',
source: ConfigSource.DEFAULT_CONFIG,
},
],
},
];
export const partitions = [
{
partition: 0,
leader: 2,
replicas: [
{
broker: 2,
leader: false,
inSync: true,
},
],
offsetMax: 0,
offsetMin: 0,
},
];
export const topicWithInfo: TopicWithDetailedInfo = {
name: topicName,
internal: false,
partitionCount: 1,
replicationFactor: 1,
replicas: 1,
inSyncReplicas: 1,
segmentSize: 0,
segmentCount: 1,
underReplicatedPartitions: 0,
cleanUpPolicy: CleanUpPolicy.DELETE,
partitions,
config,
};

View file

@ -1,64 +0,0 @@
import React from 'react';
import DangerZone, {
Props,
} from 'components/Topics/Topic/Edit/DangerZone/DangerZone';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render } from 'lib/testHelpers';
const setupWrapper = (props?: Partial<Props>) => (
<DangerZone
clusterName="testCluster"
topicName="testTopic"
defaultPartitions={3}
defaultReplicationFactor={3}
partitionsCountIncreased={false}
replicationFactorUpdated={false}
updateTopicPartitionsCount={jest.fn()}
updateTopicReplicationFactor={jest.fn()}
{...props}
/>
);
describe('DangerZone', () => {
it('is rendered properly', () => {
const component = render(setupWrapper());
expect(component.baseElement).toMatchSnapshot();
});
it('calls updateTopicPartitionsCount', async () => {
const mockUpdateTopicPartitionsCount = jest.fn();
render(
setupWrapper({
updateTopicPartitionsCount: mockUpdateTopicPartitionsCount,
})
);
userEvent.type(screen.getByLabelText('Number of partitions *'), '4');
userEvent.click(screen.getByTestId('partitionsSubmit'));
await waitFor(() => {
userEvent.click(screen.getAllByText('Submit')[1]);
expect(mockUpdateTopicPartitionsCount).toHaveBeenCalledTimes(1);
});
});
it('calls updateTopicReplicationFactor', async () => {
const mockUpdateTopicReplicationFactor = jest.fn();
render(
setupWrapper({
updateTopicReplicationFactor: mockUpdateTopicReplicationFactor,
})
);
userEvent.type(screen.getByLabelText('Replication Factor *'), '4');
userEvent.click(screen.getByTestId('replicationFactorSubmit'));
await waitFor(() => {
userEvent.click(screen.getAllByText('Submit')[2]);
});
await waitFor(() => {
expect(mockUpdateTopicReplicationFactor).toHaveBeenCalledTimes(1);
});
});
});

View file

@ -1,360 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DangerZone is rendered properly 1`] = `
.c6 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
padding: 0px 12px;
border: none;
border-radius: 4px;
white-space: nowrap;
background: #4F4FFF;
color: #FFFFFF;
font-size: 14px;
font-weight: 500;
height: 32px;
}
.c6:hover:enabled {
background: #1717CF;
color: #FFFFFF;
cursor: pointer;
}
.c6:active:enabled {
background: #1414B8;
color: #FFFFFF;
}
.c6:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.c6 a {
color: #FFFFFF;
}
.c6 i {
margin-right: 7px;
}
.c5 {
border: 1px #ABB5BA solid;
border-radius: 4px;
height: 32px;
width: 100%;
padding-left: 12px;
font-size: 14px;
}
.c5::-webkit-input-placeholder {
color: #ABB5BA;
font-size: 14px;
}
.c5::-moz-placeholder {
color: #ABB5BA;
font-size: 14px;
}
.c5:-ms-input-placeholder {
color: #ABB5BA;
font-size: 14px;
}
.c5::placeholder {
color: #ABB5BA;
font-size: 14px;
}
.c5:hover {
border-color: #73848C;
}
.c5:focus {
outline: none;
border-color: #454F54;
}
.c5:focus::-webkit-input-placeholder {
color: transparent;
}
.c5:focus::-moz-placeholder {
color: transparent;
}
.c5:focus:-ms-input-placeholder {
color: transparent;
}
.c5:focus::placeholder {
color: transparent;
}
.c5:disabled {
color: #ABB5BA;
border-color: #E3E6E8;
cursor: not-allowed;
}
.c5:read-only {
color: #171A1C;
border: none;
background-color: #F1F2F3;
cursor: not-allowed;
}
.c5:-moz-read-only:focus::placeholder {
color: #ABB5BA;
}
.c5:read-only:focus::placeholder {
color: #ABB5BA;
}
.c8 {
border: 1px #ABB5BA solid;
border-radius: 4px;
height: 40px;
width: 100%;
padding-left: 12px;
font-size: 14px;
}
.c8::-webkit-input-placeholder {
color: #ABB5BA;
font-size: 14px;
}
.c8::-moz-placeholder {
color: #ABB5BA;
font-size: 14px;
}
.c8:-ms-input-placeholder {
color: #ABB5BA;
font-size: 14px;
}
.c8::placeholder {
color: #ABB5BA;
font-size: 14px;
}
.c8:hover {
border-color: #73848C;
}
.c8:focus {
outline: none;
border-color: #454F54;
}
.c8:focus::-webkit-input-placeholder {
color: transparent;
}
.c8:focus::-moz-placeholder {
color: transparent;
}
.c8:focus:-ms-input-placeholder {
color: transparent;
}
.c8:focus::placeholder {
color: transparent;
}
.c8:disabled {
color: #ABB5BA;
border-color: #E3E6E8;
cursor: not-allowed;
}
.c8:read-only {
color: #171A1C;
border: none;
background-color: #F1F2F3;
cursor: not-allowed;
}
.c8:-moz-read-only:focus::placeholder {
color: #ABB5BA;
}
.c8:read-only:focus::placeholder {
color: #ABB5BA;
}
.c7 {
color: #E51A1A;
font-size: 12px;
}
.c4 {
position: relative;
}
.c3 {
font-weight: 500;
font-size: 12px;
line-height: 20px;
color: #454F54;
}
.c0 {
margin-top: 16px;
padding: 8px 16px;
border: 1px solid #E3E6E8;
box-sizing: border-box;
border-radius: 8px;
margin-bottom: 16px;
}
.c0 > div {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
gap: 8px;
}
.c1 {
color: #E51A1A;
font-size: 20px;
padding-bottom: 16px;
}
.c2 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: flex-end;
-webkit-box-align: flex-end;
-ms-flex-align: flex-end;
align-items: flex-end;
gap: 16px;
}
.c2 > *:first-child {
-webkit-box-flex: 4;
-webkit-flex-grow: 4;
-ms-flex-positive: 4;
flex-grow: 4;
}
.c2 > *:last-child {
-webkit-box-flex: 1;
-webkit-flex-grow: 1;
-ms-flex-positive: 1;
flex-grow: 1;
}
<body>
<div>
<div
class="c0"
>
<h1
class="c1"
>
Danger Zone
</h1>
<div>
<form
class="c2"
>
<div>
<label
class="c3"
for="partitions"
>
Number of partitions *
</label>
<div
class="c4"
>
<input
class="c5 c4"
id="partitions"
name="partitions"
placeholder="Number of partitions"
type="number"
/>
</div>
</div>
<div>
<button
class="c6"
data-testid="partitionsSubmit"
disabled=""
type="submit"
>
Submit
</button>
</div>
</form>
<p
class="c7"
/>
<form
class="c2"
>
<div>
<label
class="c3"
for="replicationFactor"
>
Replication Factor *
</label>
<div
class="c4"
>
<input
class="c8 c4"
id="replicationFactor"
name="replicationFactor"
placeholder="Replication Factor"
type="number"
/>
</div>
</div>
<div>
<button
class="c6"
data-testid="replicationFactorSubmit"
disabled=""
type="submit"
>
Submit
</button>
</div>
</form>
<p
class="c7"
/>
</div>
</div>
</div>
</body>
`;

View file

@ -64,13 +64,14 @@ const CustomParamField: React.FC<Props> = ({
control={control}
rules={{ required: 'Custom Parameter is required.' }}
name={`customParams.${index}.name`}
render={({ field: { name, onChange } }) => (
render={({ field: { value, name, onChange } }) => (
<Select
name={name}
placeholder="Select"
disabled={isDisabled}
minWidth="270px"
onChange={onChange}
value={value}
options={Object.keys(TOPIC_CUSTOM_PARAMS)
.sort()
.map((opt) => ({

View file

@ -1,27 +1,25 @@
import React from 'react';
import { TopicConfigByName, TopicFormData } from 'redux/interfaces';
import { TopicFormData } from 'redux/interfaces';
import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
import { Button } from 'components/common/Button/Button';
import { TOPIC_CUSTOM_PARAMS_PREFIX } from 'lib/constants';
import CustomParamField from './CustomParamField';
import * as S from './CustomParams.styled';
export const INDEX_PREFIX = 'customParams';
export interface CustomParamsProps {
isSubmitting: boolean;
config?: TopicConfigByName;
}
const CustomParams: React.FC<CustomParamsProps> = ({ isSubmitting }) => {
const { control } = useFormContext<TopicFormData>();
const { fields, append, remove } = useFieldArray({
control,
name: INDEX_PREFIX,
name: TOPIC_CUSTOM_PARAMS_PREFIX,
});
const watchFieldArray = useWatch({
control,
name: INDEX_PREFIX,
name: TOPIC_CUSTOM_PARAMS_PREFIX,
defaultValue: fields,
});
const controlledFields = fields.map((field, index) => {

View file

@ -8,6 +8,8 @@ import { FormProvider, useForm } from 'react-hook-form';
import userEvent from '@testing-library/user-event';
import { TOPIC_CUSTOM_PARAMS } from 'lib/constants';
import { defaultValues } from './fixtures';
const selectOption = async (listbox: HTMLElement, option: string) => {
await waitFor(() => {
userEvent.click(listbox);
@ -44,10 +46,9 @@ const expectOptionAvailability = async (
await waitFor(() => userEvent.click(listbox));
};
describe('CustomParams', () => {
const setupComponent = (props: CustomParamsProps) => {
const renderComponent = (props: CustomParamsProps, defaults = {}) => {
const Wrapper: React.FC = ({ children }) => {
const methods = useForm();
const methods = useForm({ defaultValues: defaults });
return <FormProvider {...methods}>{children}</FormProvider>;
};
@ -56,19 +57,33 @@ describe('CustomParams', () => {
<CustomParams {...props} />
</Wrapper>
);
};
beforeEach(() => {
setupComponent({ isSubmitting: false });
});
};
describe('CustomParams', () => {
it('renders with props', () => {
renderComponent({ isSubmitting: false });
const button = screen.getByRole('button');
expect(button).toBeInTheDocument();
expect(button).toHaveTextContent('Add Custom Parameter');
});
it('has defaultValues when they are set', () => {
renderComponent({ isSubmitting: false }, defaultValues);
expect(
screen.getByRole('option', { name: defaultValues.customParams[0].name })
).toBeInTheDocument();
expect(screen.getByRole('textbox')).toHaveValue(
defaultValues.customParams[0].value
);
});
describe('works with user inputs correctly', () => {
beforeEach(() => {
renderComponent({ isSubmitting: false });
});
it('button click creates custom param fieldset', async () => {
const button = screen.getByRole('button');
await waitFor(() => userEvent.click(button));

View file

@ -0,0 +1,15 @@
export const defaultValues = {
partitions: 1,
replicationFactor: 1,
minInSyncReplicas: 1,
cleanupPolicy: 'delete',
retentionBytes: -1,
maxMessageBytes: 1000012,
name: 'TestCustomParamEdit',
customParams: [
{
name: 'delete.retention.ms',
value: '86400001',
},
],
};

View file

@ -1,7 +1,7 @@
import React from 'react';
import { useFormContext, Controller } from 'react-hook-form';
import { NOT_SET, BYTES_IN_GB } from 'lib/constants';
import { TopicName, TopicConfigByName } from 'redux/interfaces';
import { TopicName } from 'redux/interfaces';
import { ErrorMessage } from '@hookform/error-message';
import Select, { SelectOption } from 'components/common/Select/Select';
import Input from 'components/common/Input/Input';
@ -16,7 +16,6 @@ import * as S from './TopicForm.styled';
export interface Props {
topicName?: TopicName;
config?: TopicConfigByName;
isEditing?: boolean;
isSubmitting: boolean;
onSubmit: (e: React.BaseSyntheticEvent) => Promise<void>;
@ -38,7 +37,6 @@ const RetentionBytesOptions: Array<SelectOption> = [
const TopicForm: React.FC<Props> = ({
topicName,
config,
isEditing,
isSubmitting,
onSubmit,
@ -197,7 +195,7 @@ const TopicForm: React.FC<Props> = ({
</S.Column>
<S.CustomParamsHeading>Custom parameters</S.CustomParamsHeading>
<CustomParamsContainer isSubmitting={isSubmitting} config={config} />
<CustomParamsContainer isSubmitting={isSubmitting} />
<Button type="submit" buttonType="primary" buttonSize="L">
Send

View file

@ -17,6 +17,7 @@ export const BASE_PARAMS: ConfigurationParameters = {
export const TOPIC_NAME_VALIDATION_PATTERN = /^[.,A-Za-z0-9_-]+$/;
export const SCHEMA_NAME_VALIDATION_PATTERN = /^[.,A-Za-z0-9_-]+$/;
export const TOPIC_CUSTOM_PARAMS_PREFIX = 'customParams';
export const TOPIC_CUSTOM_PARAMS: Record<string, string> = {
'compression.type': 'producer',
'leader.replication.throttled.replicas': '',