Merge branch 'master' into ISSUE-3427_frontend

This commit is contained in:
Roman Zabaluev 2023-05-08 12:10:31 +04:00 committed by GitHub
commit f5a29e85cd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 138 additions and 31 deletions

View file

@ -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<Cluster, unknown>;
const ClusterTableActionsCell: React.FC<Props> = ({ row }) => {
const { name } = row.original;
const { data } = useGetUserInfo();
const isApplicationConfig = useMemo(() => {
return !!data?.userInfo?.permissions.some(
(permission) => permission.resource === ResourceType.APPLICATIONCONFIG
);
}, [data]);
return (
<Button buttonType="secondary" buttonSize="S" to={clusterConfigPath(name)}>
<ActionCanButton
buttonType="secondary"
buttonSize="S"
to={clusterConfigPath(name)}
canDoAction={isApplicationConfig}
>
Configure
</Button>
</ActionCanButton>
);
};

View file

@ -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 (
<>
<PageHeading text="Dashboard" />
@ -87,9 +94,14 @@ const Dashboard: React.FC = () => {
<label>Only offline clusters</label>
</div>
{appInfo.hasDynamicConfig && (
<Button buttonType="primary" buttonSize="M" to={clusterNewConfigPath}>
<ActionCanButton
buttonType="primary"
buttonSize="M"
to={clusterNewConfigPath}
canDoAction={isApplicationConfig}
>
Configure new cluster
</Button>
</ActionCanButton>
)}
</S.Toolbar>
<Table

View file

@ -8,6 +8,7 @@ import { formatTimestamp } from 'lib/dateTimeHelpers';
import { JSONPath } from 'jsonpath-plus';
import Ellipsis from 'components/common/Ellipsis/Ellipsis';
import WarningRedIcon from 'components/common/Icons/WarningRedIcon';
import Tooltip from 'components/common/Tooltip/Tooltip';
import MessageContent from './MessageContent/MessageContent';
import * as S from './MessageContent/MessageContent.styled';
@ -110,14 +111,26 @@ const Message: React.FC<Props> = ({
</td>
<S.DataCell title={key}>
<Ellipsis text={renderFilteredJson(key, keyFilters)}>
{keySerde === 'Fallback' && <WarningRedIcon />}
{keySerde === 'Fallback' && (
<Tooltip
value={<WarningRedIcon />}
content="Fallback serde was used"
placement="left"
/>
)}
</Ellipsis>
</S.DataCell>
<S.DataCell title={content}>
<S.Metadata>
<S.MetadataValue>
<Ellipsis text={renderFilteredJson(content, contentFilters)}>
{valueSerde === 'Fallback' && <WarningRedIcon />}
{valueSerde === 'Fallback' && (
<Tooltip
value={<WarningRedIcon />}
content="Fallback serde was used"
placement="left"
/>
)}
</Ellipsis>
</S.MetadataValue>
</S.Metadata>

View file

@ -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"
/>
)}
/>

View file

@ -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<InputProps>(

View file

@ -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,7 +100,8 @@ function pasteNumberCheck(
return value;
}
const Input: React.FC<InputProps> = ({
const Input = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {
const {
name,
hookFormOptions,
search,
@ -110,8 +112,10 @@ const Input: React.FC<InputProps> = ({
withError = false,
label,
hint,
clearIcon,
...rest
}) => {
} = props;
const methods = useFormContext();
const fieldId = React.useId();
@ -168,7 +172,6 @@ const Input: React.FC<InputProps> = ({
// if the field is a part of react-hook-form form
inputOptions = { ...rest, ...methods.register(name, hookFormOptions) };
}
return (
<div>
{label && <InputLabel htmlFor={rest.id || fieldId}>{label}</InputLabel>}
@ -181,8 +184,11 @@ const Input: React.FC<InputProps> = ({
type={type}
onKeyPress={keyPressEventHandler}
onPaste={pasteEventHandler}
ref={ref}
{...inputOptions}
/>
{clearIcon}
{withError && isHookFormField && (
<S.FormError>
<ErrorMessage name={name} />
@ -192,6 +198,6 @@ const Input: React.FC<InputProps> = ({
</S.Wrapper>
</div>
);
};
});
export default Input;

View file

@ -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<SearchProps> = ({
placeholder = 'Search',
disabled = false,
@ -17,7 +29,11 @@ const Search: React.FC<SearchProps> = ({
onChange,
}) => {
const [searchParams, setSearchParams] = useSearchParams();
const ref = useRef<HTMLInputElement>(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<SearchProps> = ({
setSearchParams(searchParams);
}
}, 500);
const clearSearchValue = () => {
if (searchParams.get('q')) {
searchParams.set('q', '');
setSearchParams(searchParams);
}
if (ref.current != null) {
ref.current.value = '';
}
};
return (
<Input
@ -37,7 +62,13 @@ const Search: React.FC<SearchProps> = ({
defaultValue={value || searchParams.get('q') || ''}
inputSize="M"
disabled={disabled}
ref={ref}
search
clearIcon={
<IconButtonWrapper onClick={clearSearchValue}>
<CloseIcon />
</IconButtonWrapper>
}
/>
);
};

View file

@ -41,4 +41,24 @@ describe('Search', () => {
render(<Search />);
expect(screen.queryByPlaceholderText('Search')).toBeInTheDocument();
});
it('Clear button is visible', () => {
render(<Search placeholder={placeholder} />);
const clearButton = screen.getByRole('button');
expect(clearButton).toBeInTheDocument();
});
it('Clear button should clear text from input', async () => {
render(<Search placeholder={placeholder} />);
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('');
});
});

View file

@ -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 '';