123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313 |
- 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<ClusterGroupParam>();
- 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<string[]>([]);
- const [selectedPartitions, setSelectedPartitions] = React.useState<Option[]>(
- []
- );
- const methods = useForm<FormType>({
- 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 <PageLoader />;
- }
- return (
- <FormProvider {...methods}>
- <PageHeading text="Reset offsets" />
- <S.Wrapper>
- <form onSubmit={handleSubmit(onSubmit)}>
- <S.MainSelectors>
- <div>
- <InputLabel id="topicLabel">Topic</InputLabel>
- <Controller
- control={control}
- name="topic"
- render={({ field: { name, onChange, value } }) => (
- <Select
- id="topic"
- selectSize="M"
- aria-labelledby="topicLabel"
- minWidth="100%"
- name={name}
- onChange={onChange}
- defaultValue={value}
- value={value}
- options={uniqueTopics.map((topic) => ({
- value: topic,
- label: topic,
- }))}
- />
- )}
- />
- </div>
- <div>
- <InputLabel id="resetTypeLabel">Reset Type</InputLabel>
- <Controller
- control={control}
- name="resetType"
- render={({ field: { name, onChange, value } }) => (
- <Select
- id="resetType"
- selectSize="M"
- aria-labelledby="resetTypeLabel"
- minWidth="100%"
- name={name}
- onChange={onChange}
- value={value}
- options={Object.values(ConsumerGroupOffsetsResetType).map(
- (type) => ({ value: type, label: type })
- )}
- />
- )}
- />
- </div>
- <div>
- <InputLabel>Partitions</InputLabel>
- <MultiSelect
- options={
- consumerGroup.partitions
- ?.filter((p) => p.topic === topicValue)
- .map((p) => ({
- label: `Partition #${p.partition.toString()}`,
- value: p.partition,
- })) || []
- }
- value={selectedPartitions}
- onChange={onSelectedPartitionsChange}
- labelledBy="Select partitions"
- />
- </div>
- </S.MainSelectors>
- {resetTypeValue === ConsumerGroupOffsetsResetType.TIMESTAMP &&
- selectedPartitions.length > 0 && (
- <div>
- <InputLabel>Timestamp</InputLabel>
- <Controller
- control={control}
- name="resetToTimestamp"
- render={({ field: { onChange, onBlur, value, ref } }) => (
- <DatePicker
- ref={ref}
- selected={value}
- onChange={onChange}
- onBlur={onBlur}
- showTimeInput
- timeInputLabel="Time:"
- dateFormat="MMMM d, yyyy h:mm aa"
- />
- )}
- />
- <ErrorMessage
- errors={errors}
- name="resetToTimestamp"
- render={({ message }) => <FormError>{message}</FormError>}
- />
- </div>
- )}
- {resetTypeValue === ConsumerGroupOffsetsResetType.OFFSET &&
- selectedPartitions.length > 0 && (
- <div>
- <S.OffsetsTitle>Offsets</S.OffsetsTitle>
- <S.OffsetsWrapper>
- {fields.map((field, index) => (
- <div key={field.id}>
- <InputLabel htmlFor={`partitionsOffsets.${index}.offset`}>
- Partition #{field.partition}
- </InputLabel>
- <Input
- id={`partitionsOffsets.${index}.offset`}
- type="number"
- name={`partitionsOffsets.${index}.offset` as const}
- hookFormOptions={{
- shouldUnregister: true,
- min: {
- value: 0,
- message: 'must be greater than or equal to 0',
- },
- }}
- defaultValue={field.offset}
- />
- <ErrorMessage
- errors={errors}
- name={`partitionsOffsets.${index}.offset`}
- render={({ message }) => (
- <FormError>{message}</FormError>
- )}
- />
- </div>
- ))}
- </S.OffsetsWrapper>
- </div>
- )}
- <Button
- buttonSize="M"
- buttonType="primary"
- type="submit"
- disabled={!isValid || selectedPartitions.length === 0}
- >
- Submit
- </Button>
- </form>
- </S.Wrapper>
- </FormProvider>
- );
- };
- export default ResetOffsets;
|