diff --git a/kafka-ui-react-app/src/components/Dashboard/ClusterTableActionsCell.tsx b/kafka-ui-react-app/src/components/Dashboard/ClusterTableActionsCell.tsx index cb41ab06a8..19fefd784c 100644 --- a/kafka-ui-react-app/src/components/Dashboard/ClusterTableActionsCell.tsx +++ b/kafka-ui-react-app/src/components/Dashboard/ClusterTableActionsCell.tsx @@ -1,17 +1,31 @@ -import React from 'react'; -import { Cluster } from 'generated-sources'; +import React, { useMemo } from 'react'; +import { Cluster, ResourceType } from 'generated-sources'; import { CellContext } from '@tanstack/react-table'; -import { Button } from 'components/common/Button/Button'; import { clusterConfigPath } from 'lib/paths'; +import { useGetUserInfo } from 'lib/hooks/api/roles'; +import { ActionCanButton } from 'components/common/ActionComponent'; type Props = CellContext; const ClusterTableActionsCell: React.FC = ({ row }) => { const { name } = row.original; + const { data } = useGetUserInfo(); + + const isApplicationConfig = useMemo(() => { + return !!data?.userInfo?.permissions.some( + (permission) => permission.resource === ResourceType.APPLICATIONCONFIG + ); + }, [data]); + return ( - + ); }; diff --git a/kafka-ui-react-app/src/components/Dashboard/Dashboard.tsx b/kafka-ui-react-app/src/components/Dashboard/Dashboard.tsx index 7eab4c1d2f..c7b64aef1c 100644 --- a/kafka-ui-react-app/src/components/Dashboard/Dashboard.tsx +++ b/kafka-ui-react-app/src/components/Dashboard/Dashboard.tsx @@ -1,23 +1,25 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo } from 'react'; import PageHeading from 'components/common/PageHeading/PageHeading'; import * as Metrics from 'components/common/Metrics'; import { Tag } from 'components/common/Tag/Tag.styled'; import Switch from 'components/common/Switch/Switch'; import { useClusters } from 'lib/hooks/api/clusters'; -import { Cluster, ServerStatus } from 'generated-sources'; +import { Cluster, ResourceType, ServerStatus } from 'generated-sources'; import { ColumnDef } from '@tanstack/react-table'; import Table, { SizeCell } from 'components/common/NewTable'; import useBoolean from 'lib/hooks/useBoolean'; -import { Button } from 'components/common/Button/Button'; import { clusterNewConfigPath } from 'lib/paths'; import { GlobalSettingsContext } from 'components/contexts/GlobalSettingsContext'; import { useNavigate } from 'react-router-dom'; +import { ActionCanButton } from 'components/common/ActionComponent'; +import { useGetUserInfo } from 'lib/hooks/api/roles'; import * as S from './Dashboard.styled'; import ClusterName from './ClusterName'; import ClusterTableActionsCell from './ClusterTableActionsCell'; const Dashboard: React.FC = () => { + const { data } = useGetUserInfo(); const clusters = useClusters(); const { value: showOfflineOnly, toggle } = useBoolean(false); const appInfo = React.useContext(GlobalSettingsContext); @@ -62,6 +64,11 @@ const Dashboard: React.FC = () => { } }, [clusters, appInfo.hasDynamicConfig]); + const isApplicationConfig = useMemo(() => { + return !!data?.userInfo?.permissions.some( + (permission) => permission.resource === ResourceType.APPLICATIONCONFIG + ); + }, [data]); return ( <> @@ -87,9 +94,14 @@ const Dashboard: React.FC = () => { {appInfo.hasDynamicConfig && ( - + )} = ({ - {keySerde === 'Fallback' && } + {keySerde === 'Fallback' && ( + } + content="Fallback serde was used" + placement="left" + /> + )} - {valueSerde === 'Fallback' && } + {valueSerde === 'Fallback' && ( + } + content="Fallback serde was used" + placement="left" + /> + )} diff --git a/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx b/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx index bacfa76c93..b7f31a230b 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx @@ -210,6 +210,7 @@ const SendMessage: React.FC<{ closeSidebar: () => void }> = ({ name={name} onChange={onChange} value={value} + height="40px" /> )} /> @@ -225,6 +226,7 @@ const SendMessage: React.FC<{ closeSidebar: () => void }> = ({ name={name} onChange={onChange} value={value} + height="280px" /> )} /> @@ -242,7 +244,7 @@ const SendMessage: React.FC<{ closeSidebar: () => void }> = ({ defaultValue="{}" name={name} onChange={onChange} - height="200px" + height="40px" /> )} /> diff --git a/kafka-ui-react-app/src/components/common/Input/Input.styled.ts b/kafka-ui-react-app/src/components/common/Input/Input.styled.ts index 9495aaecbe..f21962fe6b 100644 --- a/kafka-ui-react-app/src/components/common/Input/Input.styled.ts +++ b/kafka-ui-react-app/src/components/common/Input/Input.styled.ts @@ -29,6 +29,16 @@ export const Wrapper = styled.div` width: 16px; fill: ${({ theme }) => theme.input.icon.color}; } + svg:last-child { + position: absolute; + top: 8px; + line-height: 0; + z-index: 1; + left: unset; + right: 12px; + height: 16px; + width: 16px; + } `; export const Input = styled.input( diff --git a/kafka-ui-react-app/src/components/common/Input/Input.tsx b/kafka-ui-react-app/src/components/common/Input/Input.tsx index ae76bc4717..4d04b730e5 100644 --- a/kafka-ui-react-app/src/components/common/Input/Input.tsx +++ b/kafka-ui-react-app/src/components/common/Input/Input.tsx @@ -16,6 +16,7 @@ export interface InputProps withError?: boolean; label?: React.ReactNode; hint?: React.ReactNode; + clearIcon?: React.ReactNode; // Some may only accept integer, like `Number of Partitions` // some may accept decimal @@ -99,19 +100,22 @@ function pasteNumberCheck( return value; } -const Input: React.FC = ({ - name, - hookFormOptions, - search, - inputSize = 'L', - type, - positiveOnly, - integerOnly, - withError = false, - label, - hint, - ...rest -}) => { +const Input = React.forwardRef((props, ref) => { + const { + name, + hookFormOptions, + search, + inputSize = 'L', + type, + positiveOnly, + integerOnly, + withError = false, + label, + hint, + clearIcon, + ...rest + } = props; + const methods = useFormContext(); const fieldId = React.useId(); @@ -168,7 +172,6 @@ const Input: React.FC = ({ // if the field is a part of react-hook-form form inputOptions = { ...rest, ...methods.register(name, hookFormOptions) }; } - return (
{label && {label}} @@ -181,8 +184,11 @@ const Input: React.FC = ({ type={type} onKeyPress={keyPressEventHandler} onPaste={pasteEventHandler} + ref={ref} {...inputOptions} /> + {clearIcon} + {withError && isHookFormField && ( @@ -192,6 +198,6 @@ const Input: React.FC = ({
); -}; +}); export default Input; diff --git a/kafka-ui-react-app/src/components/common/Search/Search.tsx b/kafka-ui-react-app/src/components/common/Search/Search.tsx index 66c0e95030..65116d645a 100644 --- a/kafka-ui-react-app/src/components/common/Search/Search.tsx +++ b/kafka-ui-react-app/src/components/common/Search/Search.tsx @@ -1,7 +1,9 @@ -import React from 'react'; +import React, { useRef } from 'react'; import { useDebouncedCallback } from 'use-debounce'; import Input from 'components/common/Input/Input'; import { useSearchParams } from 'react-router-dom'; +import CloseIcon from 'components/common/Icons/CloseIcon'; +import styled from 'styled-components'; interface SearchProps { placeholder?: string; @@ -10,6 +12,16 @@ interface SearchProps { value?: string; } +const IconButtonWrapper = styled.span.attrs(() => ({ + role: 'button', + tabIndex: '0', +}))` + height: 16px !important; + display: inline-block; + &:hover { + cursor: pointer; + } +`; const Search: React.FC = ({ placeholder = 'Search', disabled = false, @@ -17,7 +29,11 @@ const Search: React.FC = ({ onChange, }) => { const [searchParams, setSearchParams] = useSearchParams(); + const ref = useRef(null); const handleChange = useDebouncedCallback((e) => { + if (ref.current != null) { + ref.current.value = e.target.value; + } if (onChange) { onChange(e.target.value); } else { @@ -28,6 +44,15 @@ const Search: React.FC = ({ setSearchParams(searchParams); } }, 500); + const clearSearchValue = () => { + if (searchParams.get('q')) { + searchParams.set('q', ''); + setSearchParams(searchParams); + } + if (ref.current != null) { + ref.current.value = ''; + } + }; return ( = ({ defaultValue={value || searchParams.get('q') || ''} inputSize="M" disabled={disabled} + ref={ref} search + clearIcon={ + + + + } /> ); }; diff --git a/kafka-ui-react-app/src/components/common/Search/__tests__/Search.spec.tsx b/kafka-ui-react-app/src/components/common/Search/__tests__/Search.spec.tsx index 808f229317..2103d22336 100644 --- a/kafka-ui-react-app/src/components/common/Search/__tests__/Search.spec.tsx +++ b/kafka-ui-react-app/src/components/common/Search/__tests__/Search.spec.tsx @@ -41,4 +41,24 @@ describe('Search', () => { render(); expect(screen.queryByPlaceholderText('Search')).toBeInTheDocument(); }); + + it('Clear button is visible', () => { + render(); + + const clearButton = screen.getByRole('button'); + expect(clearButton).toBeInTheDocument(); + }); + + it('Clear button should clear text from input', async () => { + render(); + + const searchField = screen.getAllByRole('textbox')[0]; + await userEvent.type(searchField, 'some text'); + expect(searchField).toHaveValue('some text'); + + const clearButton = screen.getByRole('button'); + await userEvent.click(clearButton); + + expect(searchField).toHaveValue(''); + }); }); diff --git a/kafka-ui-react-app/src/lib/dateTimeHelpers.ts b/kafka-ui-react-app/src/lib/dateTimeHelpers.ts index 3dce0edd78..148a70d2a3 100644 --- a/kafka-ui-react-app/src/lib/dateTimeHelpers.ts +++ b/kafka-ui-react-app/src/lib/dateTimeHelpers.ts @@ -1,6 +1,6 @@ export const formatTimestamp = ( timestamp?: number | string | Date, - format: Intl.DateTimeFormatOptions = { hour12: false } + format: Intl.DateTimeFormatOptions = { hourCycle: 'h23' } ): string => { if (!timestamp) { return ''; @@ -8,7 +8,6 @@ export const formatTimestamp = ( // empty array gets the default one from the browser const date = new Date(timestamp); - // invalid date if (Number.isNaN(date.getTime())) { return '';