Skip to content

Commit 8d3bac8

Browse files
authored
[FE} Refactor Consumers Section (#3508)
* refactor CG List & details page * Refactor ResetOffset page * get rid of redux reducer
1 parent 84d3b32 commit 8d3bac8

File tree

22 files changed

+402
-1104
lines changed

22 files changed

+402
-1104
lines changed

kafka-ui-react-app/src/components/ConsumerGroups/ConsumerGroups.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
import React from 'react';
22
import { Route, Routes } from 'react-router-dom';
33
import Details from 'components/ConsumerGroups/Details/Details';
4-
import ListContainer from 'components/ConsumerGroups/List/ListContainer';
54
import ResetOffsets from 'components/ConsumerGroups/Details/ResetOffsets/ResetOffsets';
65
import {
76
clusterConsumerGroupResetOffsetsRelativePath,
87
RouteParams,
98
} from 'lib/paths';
109

10+
import List from './List';
11+
1112
const ConsumerGroups: React.FC = () => {
1213
return (
1314
<Routes>
14-
<Route index element={<ListContainer />} />
15+
<Route index element={<List />} />
1516
<Route path={RouteParams.consumerGroupID} element={<Details />} />
1617
<Route
1718
path={clusterConsumerGroupResetOffsetsRelativePath}

kafka-ui-react-app/src/components/ConsumerGroups/Details/Details.tsx

Lines changed: 18 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,22 @@ import {
77
ClusterGroupParam,
88
} from 'lib/paths';
99
import Search from 'components/common/Search/Search';
10-
import PageLoader from 'components/common/PageLoader/PageLoader';
1110
import ClusterContext from 'components/contexts/ClusterContext';
1211
import PageHeading from 'components/common/PageHeading/PageHeading';
1312
import * as Metrics from 'components/common/Metrics';
1413
import { Tag } from 'components/common/Tag/Tag.styled';
1514
import groupBy from 'lodash/groupBy';
1615
import { Table } from 'components/common/table/Table/Table.styled';
17-
import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
18-
import {
19-
deleteConsumerGroup,
20-
selectById,
21-
fetchConsumerGroupDetails,
22-
getAreConsumerGroupDetailsFulfilled,
23-
} from 'redux/reducers/consumerGroups/consumerGroupsSlice';
2416
import getTagColor from 'components/common/Tag/getTagColor';
2517
import { Dropdown } from 'components/common/Dropdown';
2618
import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled';
2719
import { Action, ResourceType } from 'generated-sources';
2820
import { ActionDropdownItem } from 'components/common/ActionComponent';
2921
import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
22+
import {
23+
useConsumerGroupDetails,
24+
useDeleteConsumerGroupMutation,
25+
} from 'lib/hooks/api/consumers';
3026

3127
import ListItem from './ListItem';
3228

@@ -35,38 +31,25 @@ const Details: React.FC = () => {
3531
const [searchParams] = useSearchParams();
3632
const searchValue = searchParams.get('q') || '';
3733
const { isReadOnly } = React.useContext(ClusterContext);
38-
const { consumerGroupID, clusterName } = useAppParams<ClusterGroupParam>();
39-
const dispatch = useAppDispatch();
40-
const consumerGroup = useAppSelector((state) =>
41-
selectById(state, consumerGroupID)
42-
);
43-
const isFetched = useAppSelector(getAreConsumerGroupDetailsFulfilled);
34+
const routeParams = useAppParams<ClusterGroupParam>();
35+
const { clusterName, consumerGroupID } = routeParams;
4436

45-
React.useEffect(() => {
46-
dispatch(fetchConsumerGroupDetails({ clusterName, consumerGroupID }));
47-
}, [clusterName, consumerGroupID, dispatch]);
37+
const consumerGroup = useConsumerGroupDetails(routeParams);
38+
const deleteConsumerGroup = useDeleteConsumerGroupMutation(routeParams);
4839

4940
const onDelete = async () => {
50-
const res = await dispatch(
51-
deleteConsumerGroup({ clusterName, consumerGroupID })
52-
).unwrap();
53-
if (res) navigate('../');
41+
await deleteConsumerGroup.mutateAsync();
42+
navigate('../');
5443
};
5544

5645
const onResetOffsets = () => {
5746
navigate(clusterConsumerGroupResetRelativePath);
5847
};
5948

60-
if (!isFetched || !consumerGroup) {
61-
return <PageLoader />;
62-
}
63-
64-
const partitionsByTopic = groupBy(consumerGroup.partitions, 'topic');
65-
49+
const partitionsByTopic = groupBy(consumerGroup.data?.partitions, 'topic');
6650
const filteredPartitionsByTopic = Object.keys(partitionsByTopic).filter(
6751
(el) => el.includes(searchValue)
6852
);
69-
7053
const currentPartitionsByTopic = searchValue.length
7154
? filteredPartitionsByTopic
7255
: Object.keys(partitionsByTopic);
@@ -110,24 +93,24 @@ const Details: React.FC = () => {
11093
<Metrics.Wrapper>
11194
<Metrics.Section>
11295
<Metrics.Indicator label="State">
113-
<Tag color={getTagColor(consumerGroup.state)}>
114-
{consumerGroup.state}
96+
<Tag color={getTagColor(consumerGroup.data?.state)}>
97+
{consumerGroup.data?.state}
11598
</Tag>
11699
</Metrics.Indicator>
117100
<Metrics.Indicator label="Members">
118-
{consumerGroup.members}
101+
{consumerGroup.data?.members}
119102
</Metrics.Indicator>
120103
<Metrics.Indicator label="Assigned Topics">
121-
{consumerGroup.topics}
104+
{consumerGroup.data?.topics}
122105
</Metrics.Indicator>
123106
<Metrics.Indicator label="Assigned Partitions">
124-
{consumerGroup.partitions?.length}
107+
{consumerGroup.data?.partitions?.length}
125108
</Metrics.Indicator>
126109
<Metrics.Indicator label="Coordinator ID">
127-
{consumerGroup.coordinator?.id}
110+
{consumerGroup.data?.coordinator?.id}
128111
</Metrics.Indicator>
129112
<Metrics.Indicator label="Total lag">
130-
{consumerGroup.messagesBehind}
113+
{consumerGroup.data?.messagesBehind}
131114
</Metrics.Indicator>
132115
</Metrics.Section>
133116
</Metrics.Wrapper>
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import React from 'react';
2+
import { useNavigate } from 'react-router-dom';
3+
import {
4+
ConsumerGroupDetails,
5+
ConsumerGroupOffsetsReset,
6+
ConsumerGroupOffsetsResetType,
7+
} from 'generated-sources';
8+
import { ClusterGroupParam } from 'lib/paths';
9+
import {
10+
Controller,
11+
FormProvider,
12+
useFieldArray,
13+
useForm,
14+
} from 'react-hook-form';
15+
import { MultiSelect, Option } from 'react-multi-select-component';
16+
import 'react-datepicker/dist/react-datepicker.css';
17+
import { ErrorMessage } from '@hookform/error-message';
18+
import { InputLabel } from 'components/common/Input/InputLabel.styled';
19+
import { Button } from 'components/common/Button/Button';
20+
import Input from 'components/common/Input/Input';
21+
import { FormError } from 'components/common/Input/Input.styled';
22+
import useAppParams from 'lib/hooks/useAppParams';
23+
import { useResetConsumerGroupOffsetsMutation } from 'lib/hooks/api/consumers';
24+
import { FlexFieldset, StyledForm } from 'components/common/Form/Form.styled';
25+
import ControlledSelect from 'components/common/Select/ControlledSelect';
26+
27+
import * as S from './ResetOffsets.styled';
28+
29+
interface FormProps {
30+
defaultValues: ConsumerGroupOffsetsReset;
31+
topics: string[];
32+
partitions: ConsumerGroupDetails['partitions'];
33+
}
34+
35+
const resetTypeOptions = Object.values(ConsumerGroupOffsetsResetType).map(
36+
(value) => ({ value, label: value })
37+
);
38+
39+
const Form: React.FC<FormProps> = ({ defaultValues, partitions, topics }) => {
40+
const navigate = useNavigate();
41+
const routerParams = useAppParams<ClusterGroupParam>();
42+
const reset = useResetConsumerGroupOffsetsMutation(routerParams);
43+
const topicOptions = React.useMemo(
44+
() => topics.map((value) => ({ value, label: value })),
45+
[topics]
46+
);
47+
const methods = useForm<ConsumerGroupOffsetsReset>({
48+
mode: 'onChange',
49+
defaultValues,
50+
});
51+
52+
const {
53+
handleSubmit,
54+
setValue,
55+
watch,
56+
control,
57+
formState: { errors },
58+
} = methods;
59+
const { fields } = useFieldArray({
60+
control,
61+
name: 'partitionsOffsets',
62+
});
63+
64+
const resetTypeValue = watch('resetType');
65+
const topicValue = watch('topic');
66+
const offsetsValue = watch('partitionsOffsets');
67+
const partitionsValue = watch('partitions') || [];
68+
69+
const partitionOptions =
70+
partitions
71+
?.filter((p) => p.topic === topicValue)
72+
.map((p) => ({
73+
label: `Partition #${p.partition.toString()}`,
74+
value: p.partition,
75+
})) || [];
76+
77+
const onSelectedPartitionsChange = (selected: Option[]) => {
78+
setValue(
79+
'partitions',
80+
selected.map(({ value }) => value)
81+
);
82+
83+
setValue(
84+
'partitionsOffsets',
85+
selected.map(({ value }) => {
86+
const currentOffset = offsetsValue?.find(
87+
({ partition }) => partition === value
88+
);
89+
return { offset: currentOffset?.offset, partition: value };
90+
})
91+
);
92+
};
93+
94+
React.useEffect(() => {
95+
onSelectedPartitionsChange([]);
96+
// eslint-disable-next-line react-hooks/exhaustive-deps
97+
}, [topicValue]);
98+
99+
const onSubmit = async (data: ConsumerGroupOffsetsReset) => {
100+
await reset.mutateAsync(data);
101+
navigate('../');
102+
};
103+
104+
return (
105+
<FormProvider {...methods}>
106+
<StyledForm onSubmit={handleSubmit(onSubmit)}>
107+
<FlexFieldset>
108+
<ControlledSelect
109+
name="topic"
110+
label="Topic"
111+
placeholder="Select Topic"
112+
options={topicOptions}
113+
/>
114+
<ControlledSelect
115+
name="resetType"
116+
label="Reset Type"
117+
placeholder="Select Reset Type"
118+
options={resetTypeOptions}
119+
/>
120+
<div>
121+
<InputLabel>Partitions</InputLabel>
122+
<MultiSelect
123+
options={partitionOptions}
124+
value={partitionsValue.map((p) => ({
125+
value: p,
126+
label: String(p),
127+
}))}
128+
onChange={onSelectedPartitionsChange}
129+
labelledBy="Select partitions"
130+
/>
131+
</div>
132+
{resetTypeValue === ConsumerGroupOffsetsResetType.TIMESTAMP &&
133+
partitionsValue.length > 0 && (
134+
<div>
135+
<InputLabel>Timestamp</InputLabel>
136+
<Controller
137+
control={control}
138+
name="resetToTimestamp"
139+
rules={{
140+
required: 'Timestamp is required',
141+
}}
142+
render={({ field: { onChange, onBlur, value, ref } }) => (
143+
<S.DatePickerInput
144+
ref={ref}
145+
selected={new Date(value as number)}
146+
onChange={(e: Date | null) => onChange(e?.getTime())}
147+
onBlur={onBlur}
148+
/>
149+
)}
150+
/>
151+
<ErrorMessage
152+
errors={errors}
153+
name="resetToTimestamp"
154+
render={({ message }) => <FormError>{message}</FormError>}
155+
/>
156+
</div>
157+
)}
158+
159+
{resetTypeValue === ConsumerGroupOffsetsResetType.OFFSET &&
160+
partitionsValue.length > 0 && (
161+
<S.OffsetsWrapper>
162+
{fields.map((field, index) => (
163+
<Input
164+
key={field.id}
165+
label={`Partition #${field.partition} Offset`}
166+
type="number"
167+
name={`partitionsOffsets.${index}.offset` as const}
168+
hookFormOptions={{
169+
shouldUnregister: true,
170+
required: 'Offset is required',
171+
min: {
172+
value: 0,
173+
message: 'must be greater than or equal to 0',
174+
},
175+
}}
176+
withError
177+
/>
178+
))}
179+
</S.OffsetsWrapper>
180+
)}
181+
</FlexFieldset>
182+
<div>
183+
<Button
184+
buttonSize="M"
185+
buttonType="primary"
186+
type="submit"
187+
disabled={partitionsValue.length === 0}
188+
>
189+
Submit
190+
</Button>
191+
</div>
192+
</StyledForm>
193+
</FormProvider>
194+
);
195+
};
196+
197+
export default Form;
Lines changed: 23 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,5 @@
11
import styled from 'styled-components';
2-
3-
export const Wrapper = styled.div`
4-
padding: 16px;
5-
padding-top: 0;
6-
7-
& > form {
8-
display: flex;
9-
flex-direction: column;
10-
gap: 16px;
11-
12-
& > button:last-child {
13-
align-self: flex-start;
14-
}
15-
}
16-
17-
& .multi-select {
18-
height: 32px;
19-
& > .dropdown-container {
20-
height: 32px;
21-
& > .dropdown-heading {
22-
height: 32px;
23-
}
24-
}
25-
}
26-
`;
27-
28-
export const MainSelectors = styled.div`
29-
display: flex;
30-
gap: 16px;
31-
& > * {
32-
flex-grow: 1;
33-
}
34-
`;
2+
import DatePicker from 'react-datepicker';
353

364
export const OffsetsWrapper = styled.div`
375
display: flex;
@@ -40,7 +8,26 @@ export const OffsetsWrapper = styled.div`
408
gap: 16px;
419
`;
4210

43-
export const OffsetsTitle = styled.h1`
44-
font-size: 18px;
45-
font-weight: 500;
11+
export const DatePickerInput = styled(DatePicker).attrs({
12+
showTimeInput: true,
13+
timeInputLabel: 'Time:',
14+
dateFormat: 'MMMM d, yyyy h:mm aa',
15+
})`
16+
height: 40px;
17+
border: 1px ${({ theme }) => theme.select.borderColor.normal} solid;
18+
border-radius: 4px;
19+
font-size: 14px;
20+
width: 270px;
21+
padding-left: 12px;
22+
background-color: ${({ theme }) => theme.input.backgroundColor.normal};
23+
color: ${({ theme }) => theme.input.color.normal};
24+
&::placeholder {
25+
color: ${({ theme }) => theme.input.color.normal};
26+
}
27+
&:hover {
28+
cursor: pointer;
29+
}
30+
&:focus {
31+
outline: none;
32+
}
4633
`;

0 commit comments

Comments
 (0)