浏览代码

Refactor numeric inputs logic. (#2610)

* Prevent negative values

Closes #2451

* Add positiveOnly mode to input field

Co-authored-by: shubhwip <shubhrjain7@gmail.com>
Co-authored-by: Roman Zabaluev <rzabaluev@provectus.com>
Oleg Shur 2 年之前
父节点
当前提交
5e500636d0

+ 1 - 1
kafka-ui-react-app/src/components/Brokers/Broker/Configs/Configs.tsx

@@ -37,7 +37,7 @@ const Configs: React.FC = () => {
   const renderCell = (props: CellContext<BrokerConfig, unknown>) => (
   const renderCell = (props: CellContext<BrokerConfig, unknown>) => (
     <InputCell
     <InputCell
       {...props}
       {...props}
-      onUpdate={(name: string, value: string | null) => {
+      onUpdate={(name, value) => {
         stateMutation.mutateAsync({
         stateMutation.mutateAsync({
           name,
           name,
           brokerConfigItem: {
           brokerConfigItem: {

+ 2 - 2
kafka-ui-react-app/src/components/Brokers/Broker/Configs/InputCell.tsx

@@ -11,7 +11,7 @@ import Input from 'components/common/Input/Input';
 import * as S from './Configs.styled';
 import * as S from './Configs.styled';
 
 
 interface InputCellProps extends CellContext<BrokerConfig, unknown> {
 interface InputCellProps extends CellContext<BrokerConfig, unknown> {
-  onUpdate: (name: string, value: string | null) => void;
+  onUpdate: (name: string, value?: string) => void;
 }
 }
 
 
 const InputCell: React.FC<InputCellProps> = ({ row, getValue, onUpdate }) => {
 const InputCell: React.FC<InputCellProps> = ({ row, getValue, onUpdate }) => {
@@ -24,7 +24,7 @@ const InputCell: React.FC<InputCellProps> = ({ row, getValue, onUpdate }) => {
   const onSave = () => {
   const onSave = () => {
     if (value !== initialValue) {
     if (value !== initialValue) {
       confirm('Are you sure you want to change the value?', async () => {
       confirm('Are you sure you want to change the value?', async () => {
-        onUpdate(row?.original?.name, value || null);
+        onUpdate(row?.original?.name, value);
       });
       });
     }
     }
     setIsEdit(false);
     setIsEdit(false);

+ 56 - 30
kafka-ui-react-app/src/components/common/Input/Input.tsx

@@ -10,6 +10,7 @@ export interface InputProps
   name?: string;
   name?: string;
   hookFormOptions?: RegisterOptions;
   hookFormOptions?: RegisterOptions;
   search?: boolean;
   search?: boolean;
+  positiveOnly?: boolean;
 }
 }
 
 
 const Input: React.FC<InputProps> = ({
 const Input: React.FC<InputProps> = ({
@@ -18,42 +19,67 @@ const Input: React.FC<InputProps> = ({
   search,
   search,
   inputSize = 'L',
   inputSize = 'L',
   type,
   type,
+  positiveOnly,
   ...rest
   ...rest
 }) => {
 }) => {
   const methods = useFormContext();
   const methods = useFormContext();
+  const keyPressEventHandler = (
+    event: React.KeyboardEvent<HTMLInputElement>
+  ) => {
+    const { key, code } = event;
+    if (type === 'number') {
+      // Manualy prevent input of 'e' character for all number inputs
+      // and prevent input of negative numbers for positiveOnly inputs
+      if (key === 'e' || (positiveOnly && (key === '-' || code === 'Minus'))) {
+        event.preventDefault();
+      }
+    }
+  };
+  const pasteEventHandler = (event: React.ClipboardEvent<HTMLInputElement>) => {
+    if (type === 'number') {
+      const { clipboardData } = event;
+      const text = clipboardData.getData('Text');
+      // replace all non-digit characters with empty string
+      let value = text.replace(/[^\d.]/g, '');
+      if (positiveOnly) {
+        // check if value is negative
+        const parsedData = parseFloat(value);
+        if (parsedData < 0) {
+          // remove minus sign
+          value = String(Math.abs(parsedData));
+        }
+      }
+      // if paste value contains non-numeric characters or
+      // negative for positiveOnly fields then prevent paste
+      if (value !== text) {
+        event.preventDefault();
+
+        // for react-hook-form fields only set transformed value
+        if (name) {
+          methods.setValue(name, value);
+        }
+      }
+    }
+  };
+
+  let inputOptions = { ...rest };
+  if (name) {
+    // extend input options with react-hook-form options
+    // if the field is a part of react-hook-form form
+    inputOptions = { ...rest, ...methods.register(name, hookFormOptions) };
+  }
+
   return (
   return (
     <S.Wrapper>
     <S.Wrapper>
       {search && <SearchIcon />}
       {search && <SearchIcon />}
-      {name ? (
-        <S.Input
-          inputSize={inputSize}
-          {...methods.register(name, { ...hookFormOptions })}
-          hasLeftIcon={!!search}
-          type={type}
-          {...rest}
-          onKeyDown={(e) => {
-            if (type === 'number') {
-              if (e.key === 'e') {
-                e.preventDefault();
-              }
-            }
-          }}
-          onPaste={(e) => {
-            if (type === 'number') {
-              e.preventDefault();
-              const value = e.clipboardData.getData('Text');
-              methods.setValue(name, value.replace(/[^\d.]/g, ''));
-            }
-          }}
-        />
-      ) : (
-        <S.Input
-          inputSize={inputSize}
-          hasLeftIcon={!!search}
-          type={type}
-          {...rest}
-        />
-      )}
+      <S.Input
+        inputSize={inputSize}
+        hasLeftIcon={!!search}
+        type={type}
+        onKeyPress={keyPressEventHandler}
+        onPaste={pasteEventHandler}
+        {...inputOptions}
+      />
     </S.Wrapper>
     </S.Wrapper>
   );
   );
 };
 };

+ 22 - 1
kafka-ui-react-app/src/components/common/Input/__tests__/Input.spec.tsx

@@ -2,6 +2,7 @@ import Input, { InputProps } from 'components/common/Input/Input';
 import React from 'react';
 import React from 'react';
 import { screen } from '@testing-library/react';
 import { screen } from '@testing-library/react';
 import { render } from 'lib/testHelpers';
 import { render } from 'lib/testHelpers';
+import userEvent from '@testing-library/user-event';
 
 
 const setupWrapper = (props?: Partial<InputProps>) => (
 const setupWrapper = (props?: Partial<InputProps>) => (
   <Input name="test" {...props} />
   <Input name="test" {...props} />
@@ -11,11 +12,31 @@ jest.mock('react-hook-form', () => ({
     register: jest.fn(),
     register: jest.fn(),
   }),
   }),
 }));
 }));
+
 describe('Custom Input', () => {
 describe('Custom Input', () => {
   describe('with no icons', () => {
   describe('with no icons', () => {
+    const getInput = () => screen.getByRole('textbox');
+
     it('to be in the document', () => {
     it('to be in the document', () => {
       render(setupWrapper());
       render(setupWrapper());
-      expect(screen.getByRole('textbox')).toBeInTheDocument();
+      expect(getInput()).toBeInTheDocument();
+    });
+  });
+  describe('number', () => {
+    const getInput = () => screen.getByRole('spinbutton');
+
+    it('allows user to type only numbers', () => {
+      render(setupWrapper({ type: 'number' }));
+      const input = getInput();
+      userEvent.type(input, 'abc131');
+      expect(input).toHaveValue(131);
+    });
+
+    it('allows negative values', () => {
+      render(setupWrapper({ type: 'number' }));
+      const input = getInput();
+      userEvent.type(input, '-2');
+      expect(input).toHaveValue(-2);
     });
     });
   });
   });
 });
 });

+ 1 - 0
kafka-ui-react-app/src/components/common/NewTable/Table.tsx

@@ -329,6 +329,7 @@ const Table: React.FC<TableProps<any>> = ({
               <span>Go to page:</span>
               <span>Go to page:</span>
               <Input
               <Input
                 type="number"
                 type="number"
+                positiveOnly
                 defaultValue={table.getState().pagination.pageIndex + 1}
                 defaultValue={table.getState().pagination.pageIndex + 1}
                 inputSize="M"
                 inputSize="M"
                 max={table.getPageCount()}
                 max={table.getPageCount()}

+ 30 - 10
kafka-ui-react-app/src/components/common/NewTable/__test__/Table.spec.tsx

@@ -225,17 +225,37 @@ describe('Table', () => {
       expect(screen.getByText('lorem')).toBeInTheDocument();
       expect(screen.getByText('lorem')).toBeInTheDocument();
     });
     });
 
 
-    it('renders go to page input', async () => {
-      renderComponent({ path: '?perPage=1' });
-      // Check it renders header row and only one data row
-      expect(screen.getAllByRole('row').length).toEqual(2);
-      expect(screen.getByText('lorem')).toBeInTheDocument();
-      const input = screen.getByRole('spinbutton', { name: 'Go to page:' });
-      expect(input).toBeInTheDocument();
+    describe('Go To page', () => {
+      const getGoToPageInput = () =>
+        screen.getByRole('spinbutton', { name: 'Go to page:' });
 
 
-      userEvent.clear(input);
-      userEvent.type(input, '2');
-      expect(screen.getByText('ipsum')).toBeInTheDocument();
+      beforeEach(() => {
+        renderComponent({ path: '?perPage=1' });
+      });
+
+      it('renders Go To page', () => {
+        const goToPage = getGoToPageInput();
+        expect(goToPage).toBeInTheDocument();
+        expect(goToPage).toHaveValue(1);
+      });
+      it('updates page on Go To page change', () => {
+        const goToPage = getGoToPageInput();
+        userEvent.clear(goToPage);
+        userEvent.type(goToPage, '2');
+        expect(goToPage).toHaveValue(2);
+        expect(screen.getByText('ipsum')).toBeInTheDocument();
+      });
+      it('does not update page on Go To page change if page is out of range', () => {
+        const goToPage = getGoToPageInput();
+        userEvent.type(goToPage, '5');
+        expect(goToPage).toHaveValue(15);
+        expect(screen.getByText('No rows found')).toBeInTheDocument();
+      });
+      it('does not update page on Go To page change if page is not a number', () => {
+        const goToPage = getGoToPageInput();
+        userEvent.type(goToPage, 'abc');
+        expect(goToPage).toHaveValue(1);
+      });
     });
     });
   });
   });
 
 

+ 2 - 3
kafka-ui-react-app/src/lib/hooks/api/brokers.ts

@@ -1,12 +1,11 @@
 import { brokersApiClient as api } from 'lib/api';
 import { brokersApiClient as api } from 'lib/api';
 import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
 import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
 import { ClusterName } from 'redux/interfaces';
 import { ClusterName } from 'redux/interfaces';
+import { BrokerConfigItem } from 'generated-sources';
 
 
 interface UpdateBrokerConfigProps {
 interface UpdateBrokerConfigProps {
   name: string;
   name: string;
-  brokerConfigItem: {
-    value: string | null;
-  };
+  brokerConfigItem: BrokerConfigItem;
 }
 }
 
 
 export function useBrokers(clusterName: ClusterName) {
 export function useBrokers(clusterName: ClusterName) {