import React from 'react'; import { useNavigate } from 'react-router-dom'; import { ConsumerGroupOffsetsResetType } from 'generated-sources'; import { ClusterGroupParam } from 'lib/paths'; import { Controller, FormProvider, useFieldArray, useForm, } from 'react-hook-form'; import MultiSelect from 'react-multi-select-component'; import { Option } from 'react-multi-select-component/dist/lib/interfaces'; import DatePicker from 'react-datepicker'; import 'react-datepicker/dist/react-datepicker.css'; import groupBy from 'lodash/groupBy'; import PageLoader from 'components/common/PageLoader/PageLoader'; import { ErrorMessage } from '@hookform/error-message'; import Select from 'components/common/Select/Select'; import { InputLabel } from 'components/common/Input/InputLabel.styled'; import { Button } from 'components/common/Button/Button'; import Input from 'components/common/Input/Input'; import { FormError } from 'components/common/Input/Input.styled'; import PageHeading from 'components/common/PageHeading/PageHeading'; import { fetchConsumerGroupDetails, selectById, getAreConsumerGroupDetailsFulfilled, getIsOffsetReseted, resetConsumerGroupOffsets, } from 'redux/reducers/consumerGroups/consumerGroupsSlice'; import { useAppDispatch, useAppSelector } from 'lib/hooks/redux'; import useAppParams from 'lib/hooks/useAppParams'; import { resetLoaderById } from 'redux/reducers/loader/loaderSlice'; import * as S from './ResetOffsets.styled'; interface FormType { topic: string; resetType: ConsumerGroupOffsetsResetType; partitionsOffsets: { offset: string | undefined; partition: number }[]; resetToTimestamp: Date; } const ResetOffsets: React.FC = () => { const dispatch = useAppDispatch(); const { consumerGroupID, clusterName } = useAppParams(); const consumerGroup = useAppSelector((state) => selectById(state, consumerGroupID) ); const isFetched = useAppSelector(getAreConsumerGroupDetailsFulfilled); const isOffsetReseted = useAppSelector(getIsOffsetReseted); React.useEffect(() => { dispatch(fetchConsumerGroupDetails({ clusterName, consumerGroupID })); }, [clusterName, consumerGroupID, dispatch]); const [uniqueTopics, setUniqueTopics] = React.useState([]); const [selectedPartitions, setSelectedPartitions] = React.useState( [] ); const methods = useForm({ mode: 'onChange', defaultValues: { resetType: ConsumerGroupOffsetsResetType.EARLIEST, topic: '', partitionsOffsets: [], }, }); const { handleSubmit, setValue, watch, control, setError, clearErrors, formState: { errors, isValid }, } = methods; const { fields } = useFieldArray({ control, name: 'partitionsOffsets', }); const resetTypeValue = watch('resetType'); const topicValue = watch('topic'); const offsetsValue = watch('partitionsOffsets'); React.useEffect(() => { if (isFetched && consumerGroup?.partitions) { setValue('topic', consumerGroup.partitions[0].topic); setUniqueTopics(Object.keys(groupBy(consumerGroup.partitions, 'topic'))); } }, [consumerGroup?.partitions, isFetched, setValue]); const onSelectedPartitionsChange = (value: Option[]) => { clearErrors(); setValue( 'partitionsOffsets', value.map((partition) => { const currentOffset = offsetsValue.find( (offset) => offset.partition === partition.value ); return { offset: currentOffset ? currentOffset?.offset : undefined, partition: partition.value, }; }) ); setSelectedPartitions(value); }; React.useEffect(() => { onSelectedPartitionsChange([]); // eslint-disable-next-line react-hooks/exhaustive-deps }, [topicValue]); const onSubmit = (data: FormType) => { const augmentedData = { ...data, partitions: selectedPartitions.map((partition) => partition.value), partitionsOffsets: data.partitionsOffsets as { offset: string; partition: number; }[], }; let isValidAugmentedData = true; if (augmentedData.resetType === ConsumerGroupOffsetsResetType.OFFSET) { augmentedData.partitionsOffsets.forEach((offset, index) => { if (!offset.offset) { setError(`partitionsOffsets.${index}.offset`, { type: 'manual', message: "This field shouldn't be empty!", }); isValidAugmentedData = false; } }); } else if ( augmentedData.resetType === ConsumerGroupOffsetsResetType.TIMESTAMP ) { if (!augmentedData.resetToTimestamp) { setError(`resetToTimestamp`, { type: 'manual', message: "This field shouldn't be empty!", }); isValidAugmentedData = false; } } if (isValidAugmentedData) { dispatch( resetConsumerGroupOffsets({ clusterName, consumerGroupID, requestBody: augmentedData, }) ); } }; const navigate = useNavigate(); React.useEffect(() => { if (isOffsetReseted) { dispatch(resetLoaderById('consumerGroups/resetConsumerGroupOffsets')); navigate('../'); } }, [clusterName, consumerGroupID, dispatch, navigate, isOffsetReseted]); if (!isFetched || !consumerGroup) { return ; } return (
Topic ( ({ value: type, label: type }) )} /> )} />
Partitions p.topic === topicValue) .map((p) => ({ label: `Partition #${p.partition.toString()}`, value: p.partition, })) || [] } value={selectedPartitions} onChange={onSelectedPartitionsChange} labelledBy="Select partitions" />
{resetTypeValue === ConsumerGroupOffsetsResetType.TIMESTAMP && selectedPartitions.length > 0 && (
Timestamp ( )} /> {message}} />
)} {resetTypeValue === ConsumerGroupOffsetsResetType.OFFSET && selectedPartitions.length > 0 && (
Offsets {fields.map((field, index) => (
Partition #{field.partition} ( {message} )} />
))}
)}
); }; export default ResetOffsets;