Skip to content

Commit b0c897b

Browse files
habrahamyanproMgrdichVladSenyuta
authored
UI support for MSK (#2828)
* UI support for MSK * fixed test changes * fixed pull request comments * Use the opposite operator (!==) instead. * fixed update topic * Minor code modifications in the Edit Submission logic * minor typescript code change, in the TopicFormData * minor CustomParamField short circuit code efficiency improveral * Topic Form Edit , sanitization of Default non-dirty fields, while sending the dynamic ones * fix checkTopicCreatePossibility() * fix checkTopicCreatePossibility() Co-authored-by: Mgrdich <[email protected]> Co-authored-by: Mgrdich <[email protected]> Co-authored-by: VladSenyuta <[email protected]>
1 parent efb9ec3 commit b0c897b

File tree

16 files changed

+177
-118
lines changed

16 files changed

+177
-118
lines changed

kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/models/Topic.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
@Data
1010
@Accessors(chain = true)
1111
public class Topic {
12-
private String name, timeToRetainData, maxMessageBytes, messageKey, messageContent,
13-
partitions, customParameterValue;
12+
private String name, timeToRetainData, maxMessageBytes, messageKey, messageContent, customParameterValue;
13+
private int numberOfPartitions;
1414
private CustomParameterType customParameterType;
1515
private CleanupPolicyValue cleanupPolicyValue;
1616
private MaxSizeOnDisk maxSizeOnDisk;

kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/TopicCreateEditForm.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ public boolean isDeleteCustomParameterButtonEnabled() {
4848

4949
@Step
5050
public TopicCreateEditForm setTopicName(String topicName) {
51-
nameField.setValue(topicName);
51+
nameField.shouldBe(Condition.enabled).clear();
52+
if (topicName != null) {
53+
nameField.sendKeys(topicName);
54+
}
5255
return this;
5356
}
5457

@@ -108,8 +111,9 @@ public TopicCreateEditForm setMaxMessageBytes(String bytes) {
108111
}
109112

110113
@Step
111-
public TopicCreateEditForm setPartitions(String partitions) {
112-
partitionsField.setValue(partitions);
114+
public TopicCreateEditForm setNumberOfPartitions(int partitions) {
115+
partitionsField.shouldBe(Condition.enabled).clear();
116+
partitionsField.sendKeys(String.valueOf(partitions));
113117
return this;
114118
}
115119

kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/TopicDetails.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ public String getCleanUpPolicy() {
8585
}
8686

8787
@Step
88-
public String getPartitions() {
89-
return partitionsField.getText();
88+
public int getPartitions() {
89+
return Integer.parseInt(partitionsField.getText().trim());
9090
}
9191

9292
@Step

kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/suite/topics/TopicsTests.java

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import static com.provectus.kafka.ui.settings.Source.CLUSTER_NAME;
99
import static com.provectus.kafka.ui.utilities.FileUtils.fileToString;
1010
import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
11+
import static org.apache.commons.lang3.RandomUtils.nextInt;
1112
import static org.assertj.core.api.Assertions.assertThat;
1213

1314
import com.codeborne.selenide.Condition;
@@ -40,7 +41,7 @@ public class TopicsTests extends BaseTest {
4041
private static final String SUITE_TITLE = "Topics";
4142
private static final Topic TOPIC_TO_CREATE = new Topic()
4243
.setName("new-topic-" + randomAlphabetic(5))
43-
.setPartitions("1")
44+
.setNumberOfPartitions(1)
4445
.setCustomParameterType(COMPRESSION_TYPE)
4546
.setCustomParameterValue("producer")
4647
.setCleanupPolicyValue(DELETE);
@@ -77,7 +78,7 @@ public void createTopic() {
7778
topicCreateEditForm
7879
.waitUntilScreenReady()
7980
.setTopicName(TOPIC_TO_CREATE.getName())
80-
.setPartitions(TOPIC_TO_CREATE.getPartitions())
81+
.setNumberOfPartitions(TOPIC_TO_CREATE.getNumberOfPartitions())
8182
.selectCleanupPolicy(TOPIC_TO_CREATE.getCleanupPolicyValue())
8283
.clickCreateTopicBtn();
8384
topicDetails
@@ -92,7 +93,8 @@ public void createTopic() {
9293
.isTrue();
9394
softly.assertThat(topicDetails.getCleanUpPolicy()).as("getCleanUpPolicy()")
9495
.isEqualTo(TOPIC_TO_CREATE.getCleanupPolicyValue().toString());
95-
softly.assertThat(topicDetails.getPartitions()).as("getPartitions()").isEqualTo(TOPIC_TO_CREATE.getPartitions());
96+
softly.assertThat(topicDetails.getPartitions()).as("getPartitions()")
97+
.isEqualTo(TOPIC_TO_CREATE.getNumberOfPartitions());
9698
softly.assertAll();
9799
naviSideBar
98100
.openSideMenu(TOPICS);
@@ -237,11 +239,17 @@ void checkTopicCreatePossibility() {
237239
.waitUntilScreenReady()
238240
.clickAddTopicBtn();
239241
topicCreateEditForm
240-
.waitUntilScreenReady()
241-
.setTopicName("");
242+
.waitUntilScreenReady();
243+
assertThat(topicCreateEditForm.isCreateTopicButtonEnabled()).as("isCreateTopicButtonEnabled()").isFalse();
244+
topicCreateEditForm
245+
.setTopicName("testName");
246+
assertThat(topicCreateEditForm.isCreateTopicButtonEnabled()).as("isCreateTopicButtonEnabled()").isFalse();
247+
topicCreateEditForm
248+
.setTopicName(null)
249+
.setNumberOfPartitions(nextInt(1, 10));
242250
assertThat(topicCreateEditForm.isCreateTopicButtonEnabled()).as("isCreateTopicButtonEnabled()").isFalse();
243251
topicCreateEditForm
244-
.setTopicName("testTopic1");
252+
.setTopicName("testName");
245253
assertThat(topicCreateEditForm.isCreateTopicButtonEnabled()).as("isCreateTopicButtonEnabled()").isTrue();
246254
}
247255

kafka-ui-react-app/src/components/Topics/New/__test__/New.spec.tsx

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22
import New from 'components/Topics/New/New';
33
import { Route, Routes } from 'react-router-dom';
4-
import { act, screen, waitFor } from '@testing-library/react';
4+
import { act, screen } from '@testing-library/react';
55
import {
66
clusterTopicCopyPath,
77
clusterTopicNewPath,
@@ -13,18 +13,17 @@ import { useCreateTopic } from 'lib/hooks/api/topics';
1313

1414
const clusterName = 'local';
1515
const topicName = 'test-topic';
16+
const minValue = '1';
1617

1718
const mockNavigate = jest.fn();
1819
jest.mock('react-router-dom', () => ({
1920
...jest.requireActual('react-router-dom'),
2021
useNavigate: () => mockNavigate,
2122
}));
22-
2323
jest.mock('lib/hooks/api/topics', () => ({
2424
useCreateTopic: jest.fn(),
2525
}));
26-
27-
const renderComponent = (path: string) =>
26+
const renderComponent = (path: string) => {
2827
render(
2928
<Routes>
3029
<Route path={clusterTopicNewPath()} element={<New />} />
@@ -33,9 +32,9 @@ const renderComponent = (path: string) =>
3332
</Routes>,
3433
{ initialEntries: [path] }
3534
);
35+
};
3636

3737
const createTopicMock = jest.fn();
38-
3938
describe('New', () => {
4039
beforeEach(() => {
4140
(useCreateTopic as jest.Mock).mockImplementation(() => ({
@@ -45,7 +44,6 @@ describe('New', () => {
4544
afterEach(() => {
4645
mockNavigate.mockClear();
4746
});
48-
4947
it('checks header for create new', async () => {
5048
await act(() => {
5149
renderComponent(clusterTopicNewPath(clusterName));
@@ -57,14 +55,25 @@ describe('New', () => {
5755
renderComponent(`${clusterTopicCopyPath(clusterName)}?name=test`);
5856
expect(screen.getByRole('heading', { name: 'Copy' })).toBeInTheDocument();
5957
});
60-
6158
it('validates form', async () => {
62-
renderComponent(clusterTopicNewPath(clusterName));
59+
await renderComponent(clusterTopicNewPath(clusterName));
6360
await userEvent.type(screen.getByPlaceholderText('Topic Name'), topicName);
6461
await userEvent.clear(screen.getByPlaceholderText('Topic Name'));
65-
await waitFor(() => {
66-
expect(screen.getByText('name is a required field')).toBeInTheDocument();
67-
});
62+
await userEvent.tab();
63+
await expect(
64+
screen.getByText('name is a required field')
65+
).toBeInTheDocument();
66+
67+
await userEvent.type(
68+
screen.getByLabelText('Number of partitions *'),
69+
minValue
70+
);
71+
await userEvent.clear(screen.getByLabelText('Number of partitions *'));
72+
await userEvent.tab();
73+
await expect(
74+
screen.getByText('Number of partitions is required and must be a number')
75+
).toBeInTheDocument();
76+
6877
expect(createTopicMock).not.toHaveBeenCalled();
6978
expect(mockNavigate).not.toHaveBeenCalled();
7079
});
@@ -83,6 +92,10 @@ describe('New', () => {
8392
it('submits valid form', async () => {
8493
renderComponent(clusterTopicNewPath(clusterName));
8594
await userEvent.type(screen.getByPlaceholderText('Topic Name'), topicName);
95+
await userEvent.type(
96+
screen.getByLabelText('Number of partitions *'),
97+
minValue
98+
);
8699
await userEvent.click(screen.getByText('Create topic'));
87100
expect(createTopicMock).toHaveBeenCalledTimes(1);
88101
expect(mockNavigate).toHaveBeenLastCalledWith(`../${topicName}`);

kafka-ui-react-app/src/components/Topics/Topic/Edit/Edit.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
useUpdateTopic,
2020
} from 'lib/hooks/api/topics';
2121
import DangerZone from 'components/Topics/Topic/Edit/DangerZone/DangerZone';
22+
import { ConfigSource } from 'generated-sources';
2223

2324
export const TOPIC_EDIT_FORM_DEFAULT_PROPS = {
2425
partitions: 1,
@@ -54,16 +55,32 @@ const Edit: React.FC = () => {
5455
topicConfig?.forEach((param) => {
5556
config.byName[param.name] = param;
5657
});
57-
5858
const onSubmit = async (data: TopicFormDataRaw) => {
59-
await updateTopic.mutateAsync(data);
59+
const filteredDirtyDefaultEntries = Object.entries(data).filter(
60+
([key, val]) => {
61+
const isDirty =
62+
String(val) !==
63+
String(defaultValues[key as keyof typeof defaultValues]);
64+
65+
const isDefaultConfig =
66+
config.byName[key]?.source === ConfigSource.DEFAULT_CONFIG;
67+
68+
// if it is changed should be sent or if it was Dynamic
69+
return isDirty || !isDefaultConfig;
70+
}
71+
);
72+
73+
const newData = Object.fromEntries(filteredDirtyDefaultEntries);
74+
75+
await updateTopic.mutateAsync(newData);
6076
navigate('../');
6177
};
6278

6379
return (
6480
<>
6581
<FormProvider {...methods}>
6682
<TopicForm
83+
config={config.byName}
6784
topicName={topicName}
6885
retentionBytes={defaultValues.retentionBytes}
6986
inSyncReplicas={Number(defaultValues.minInSyncReplicas)}

kafka-ui-react-app/src/components/Topics/Topic/Edit/__test__/Edit.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ describe('Edit Component', () => {
7070
renderComponent();
7171
const btn = screen.getAllByText(/Update topic/i)[0];
7272
const field = screen.getByRole('spinbutton', {
73-
name: 'Min In Sync Replicas * Min In Sync Replicas *',
73+
name: 'Min In Sync Replicas Min In Sync Replicas',
7474
});
7575
await userEvent.type(field, '1');
7676
await userEvent.click(btn);

kafka-ui-react-app/src/components/Topics/Topic/Edit/topicParamsTransformer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const topicParamsTransformer = (topic?: Topic, config?: TopicConfig[]) => {
2020

2121
const customParams = config.reduce((acc, { name, value, defaultValue }) => {
2222
if (value === defaultValue) return acc;
23-
if (!Object.keys(TOPIC_CUSTOM_PARAMS).includes(name)) return acc;
23+
if (!TOPIC_CUSTOM_PARAMS[name]) return acc;
2424
return [...acc, { name, value }];
2525
}, [] as { name: string; value?: string }[]);
2626

kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParamField.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,20 @@ import React, { useRef } from 'react';
22
import { ErrorMessage } from '@hookform/error-message';
33
import { TOPIC_CUSTOM_PARAMS } from 'lib/constants';
44
import { FieldArrayWithId, useFormContext, Controller } from 'react-hook-form';
5-
import { TopicFormData } from 'redux/interfaces';
5+
import { TopicConfigParams, TopicFormData } from 'redux/interfaces';
66
import { InputLabel } from 'components/common/Input/InputLabel.styled';
77
import { FormError } from 'components/common/Input/Input.styled';
88
import Select from 'components/common/Select/Select';
99
import Input from 'components/common/Input/Input';
1010
import IconButtonWrapper from 'components/common/Icons/IconButtonWrapper';
1111
import CloseIcon from 'components/common/Icons/CloseIcon';
1212
import * as C from 'components/Topics/shared/Form/TopicForm.styled';
13+
import { ConfigSource } from 'generated-sources';
1314

1415
import * as S from './CustomParams.styled';
1516

1617
export interface Props {
18+
config?: TopicConfigParams;
1719
isDisabled: boolean;
1820
index: number;
1921
existingFields: string[];
@@ -27,6 +29,7 @@ const CustomParamField: React.FC<Props> = ({
2729
isDisabled,
2830
index,
2931
remove,
32+
config,
3033
existingFields,
3134
setExistingFields,
3235
}) => {
@@ -44,7 +47,10 @@ const CustomParamField: React.FC<Props> = ({
4447
.map((option) => ({
4548
value: option,
4649
label: option,
47-
disabled: existingFields.includes(option),
50+
disabled:
51+
(config &&
52+
config[option].source !== ConfigSource.DYNAMIC_TOPIC_CONFIG) ||
53+
existingFields.includes(option),
4854
}));
4955

5056
React.useEffect(() => {

kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/CustomParams.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { TopicFormData } from 'redux/interfaces';
2+
import { TopicConfigParams, TopicFormData } from 'redux/interfaces';
33
import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
44
import { Button } from 'components/common/Button/Button';
55
import { TOPIC_CUSTOM_PARAMS_PREFIX } from 'lib/constants';
@@ -9,10 +9,15 @@ import CustomParamField from './CustomParamField';
99
import * as S from './CustomParams.styled';
1010

1111
export interface CustomParamsProps {
12+
config?: TopicConfigParams;
1213
isSubmitting: boolean;
14+
isEditing?: boolean;
1315
}
1416

15-
const CustomParams: React.FC<CustomParamsProps> = ({ isSubmitting }) => {
17+
const CustomParams: React.FC<CustomParamsProps> = ({
18+
isSubmitting,
19+
config,
20+
}) => {
1621
const { control } = useFormContext<TopicFormData>();
1722
const { fields, append, remove } = useFieldArray({
1823
control,
@@ -41,9 +46,10 @@ const CustomParams: React.FC<CustomParamsProps> = ({ isSubmitting }) => {
4146

4247
return (
4348
<S.ParamsWrapper>
44-
{controlledFields.map((field, idx) => (
49+
{controlledFields?.map((field, idx) => (
4550
<CustomParamField
4651
key={field.id}
52+
config={config}
4753
field={field}
4854
remove={removeField}
4955
index={idx}

0 commit comments

Comments
 (0)