Bugfix/select (#1397)
* new select styles init * Custom select compound component * Select component fix styles * Added react hook form controller * Moved from compound component * fixed vslues & onChange for controller * fixed tests & code cleanup * fix review * fixed linter * fixed discussions Co-authored-by: Ekaterina Petrova <epetrova@provectus.com>
This commit is contained in:
parent
2b79fee1e4
commit
205d8d000d
25 changed files with 674 additions and 412 deletions
|
@ -227,6 +227,11 @@ exports[`Connectors ListItem matches snapshot 1`] = `
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"selectStyles": Object {
|
"selectStyles": Object {
|
||||||
|
"backgroundColor": Object {
|
||||||
|
"active": "#E3E6E8",
|
||||||
|
"hover": "#E3E6E8",
|
||||||
|
"normal": "#FFFFFF",
|
||||||
|
},
|
||||||
"borderColor": Object {
|
"borderColor": Object {
|
||||||
"active": "#454F54",
|
"active": "#454F54",
|
||||||
"disabled": "#E3E6E8",
|
"disabled": "#E3E6E8",
|
||||||
|
|
|
@ -112,6 +112,11 @@ const New: React.FC<NewProps> = ({
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const connectOptions = connects.map(({ name: connectName }) => ({
|
||||||
|
value: connectName,
|
||||||
|
label: connectName,
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<PageHeading text="Create new connector" />
|
<PageHeading text="Create new connector" />
|
||||||
|
@ -121,13 +126,21 @@ const New: React.FC<NewProps> = ({
|
||||||
>
|
>
|
||||||
<div className={['field', connectNameFieldClassName].join(' ')}>
|
<div className={['field', connectNameFieldClassName].join(' ')}>
|
||||||
<InputLabel>Connect *</InputLabel>
|
<InputLabel>Connect *</InputLabel>
|
||||||
<Select selectSize="M" name="connectName" disabled={isSubmitting}>
|
<Controller
|
||||||
{connects.map(({ name }) => (
|
control={control}
|
||||||
<option key={name} value={name}>
|
name="connectName"
|
||||||
{name}
|
render={({ field: { name, onChange } }) => (
|
||||||
</option>
|
<Select
|
||||||
))}
|
selectSize="M"
|
||||||
</Select>
|
name={name}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
onChange={onChange}
|
||||||
|
value={connectOptions[0].value}
|
||||||
|
minWidth="100%"
|
||||||
|
options={connectOptions}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FormError>
|
<FormError>
|
||||||
<ErrorMessage errors={errors} name="connectName" />
|
<ErrorMessage errors={errors} name="connectName" />
|
||||||
</FormError>
|
</FormError>
|
||||||
|
|
|
@ -77,9 +77,7 @@ const Details: React.FC = () => {
|
||||||
<PageHeading text={consumerGroupID}>
|
<PageHeading text={consumerGroupID}>
|
||||||
{!isReadOnly && (
|
{!isReadOnly && (
|
||||||
<Dropdown label={<VerticalElipsisIcon />} right>
|
<Dropdown label={<VerticalElipsisIcon />} right>
|
||||||
<DropdownItem onClick={onResetOffsets}>
|
<DropdownItem onClick={onResetOffsets}>Reset offset</DropdownItem>
|
||||||
Reset offsets
|
|
||||||
</DropdownItem>
|
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
style={{ color: Colors.red[50] }}
|
style={{ color: Colors.red[50] }}
|
||||||
onClick={() => setIsConfirmationModalVisible(true)}
|
onClick={() => setIsConfirmationModalVisible(true)}
|
||||||
|
|
|
@ -182,24 +182,47 @@ const ResetOffsets: React.FC = () => {
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<MainSelectorsWrapperStyled>
|
<MainSelectorsWrapperStyled>
|
||||||
<div>
|
<div>
|
||||||
<InputLabel htmlFor="topic">Topic</InputLabel>
|
<InputLabel id="topicLabel">Topic</InputLabel>
|
||||||
<Select name="topic" id="topic" selectSize="M">
|
<Controller
|
||||||
{uniqueTopics.map((topic) => (
|
control={control}
|
||||||
<option key={topic} value={topic}>
|
name="topic"
|
||||||
{topic}
|
render={({ field: { name, onChange, value } }) => (
|
||||||
</option>
|
<Select
|
||||||
))}
|
id="topic"
|
||||||
</Select>
|
selectSize="M"
|
||||||
|
aria-labelledby="topicLabel"
|
||||||
|
minWidth="100%"
|
||||||
|
name={name}
|
||||||
|
onChange={onChange}
|
||||||
|
value={value}
|
||||||
|
options={uniqueTopics.map((topic) => ({
|
||||||
|
value: topic,
|
||||||
|
label: topic,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<InputLabel htmlFor="resetType">Reset Type</InputLabel>
|
<InputLabel id="resetTypeLabel">Reset Type</InputLabel>
|
||||||
<Select name="resetType" id="resetType" selectSize="M">
|
<Controller
|
||||||
{Object.values(ConsumerGroupOffsetsResetType).map((type) => (
|
control={control}
|
||||||
<option key={type} value={type}>
|
name="resetType"
|
||||||
{type}
|
render={({ field: { name, onChange, value } }) => (
|
||||||
</option>
|
<Select
|
||||||
))}
|
id="resetType"
|
||||||
</Select>
|
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>
|
||||||
<div>
|
<div>
|
||||||
<InputLabel>Partitions</InputLabel>
|
<InputLabel>Partitions</InputLabel>
|
||||||
|
|
|
@ -37,7 +37,8 @@ const resetConsumerGroupOffsetsMockCalled = () =>
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
|
|
||||||
const selectresetTypeAndPartitions = async (resetType: string) => {
|
const selectresetTypeAndPartitions = async (resetType: string) => {
|
||||||
userEvent.selectOptions(screen.getByLabelText('Reset Type'), resetType);
|
userEvent.click(screen.getByLabelText('Reset Type'));
|
||||||
|
userEvent.click(screen.getByText(resetType));
|
||||||
userEvent.click(screen.getByText('Select...'));
|
userEvent.click(screen.getByText('Select...'));
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
userEvent.click(screen.getByText('Partition #0'));
|
userEvent.click(screen.getByText('Partition #0'));
|
||||||
|
@ -48,7 +49,9 @@ const resetConsumerGroupOffsetsWith = async (
|
||||||
resetType: string,
|
resetType: string,
|
||||||
offset: null | number = null
|
offset: null | number = null
|
||||||
) => {
|
) => {
|
||||||
userEvent.selectOptions(screen.getByLabelText('Reset Type'), resetType);
|
userEvent.click(screen.getByLabelText('Reset Type'));
|
||||||
|
const options = screen.getAllByText(resetType);
|
||||||
|
userEvent.click(options.length > 1 ? options[1] : options[0]);
|
||||||
userEvent.click(screen.getByText('Select...'));
|
userEvent.click(screen.getByText('Select...'));
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
userEvent.click(screen.getByText('Partition #0'));
|
userEvent.click(screen.getByText('Partition #0'));
|
||||||
|
|
|
@ -41,7 +41,7 @@ describe('Details component', () => {
|
||||||
fetchMock.reset();
|
fetchMock.reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when consumer gruops are NOT fetched', () => {
|
describe('when consumer groups are NOT fetched', () => {
|
||||||
it('renders progress bar for initial state', () => {
|
it('renders progress bar for initial state', () => {
|
||||||
fetchMock.getOnce(
|
fetchMock.getOnce(
|
||||||
`/api/clusters/${clusterName}/consumer-groups/${groupId}`,
|
`/api/clusters/${clusterName}/consumer-groups/${groupId}`,
|
||||||
|
@ -73,8 +73,8 @@ describe('Details component', () => {
|
||||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles [Reset offsets] click', async () => {
|
it('handles [Reset offset] click', async () => {
|
||||||
userEvent.click(screen.getByText('Reset offsets'));
|
userEvent.click(screen.getByText('Reset offset'));
|
||||||
expect(history.location.pathname).toEqual(
|
expect(history.location.pathname).toEqual(
|
||||||
clusterConsumerGroupResetOffsetsPath(clusterName, groupId)
|
clusterConsumerGroupResetOffsetsPath(clusterName, groupId)
|
||||||
);
|
);
|
||||||
|
@ -90,7 +90,7 @@ describe('Details component', () => {
|
||||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('hanles [Delete consumer group] click', async () => {
|
it('handles [Delete consumer group] click', async () => {
|
||||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||||
userEvent.click(screen.getByText('Delete consumer group'));
|
userEvent.click(screen.getByText('Delete consumer group'));
|
||||||
|
|
||||||
|
|
|
@ -85,35 +85,44 @@ const Edit: React.FC = () => {
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<InputLabel>Type</InputLabel>
|
<InputLabel>Type</InputLabel>
|
||||||
<Select
|
<Controller
|
||||||
|
control={control}
|
||||||
|
rules={{ required: true }}
|
||||||
name="schemaType"
|
name="schemaType"
|
||||||
required
|
render={({ field: { name, onChange } }) => (
|
||||||
defaultValue={schema.schemaType}
|
<Select
|
||||||
disabled={isSubmitting}
|
name={name}
|
||||||
>
|
value={schema.schemaType}
|
||||||
{Object.keys(SchemaType).map((type: string) => (
|
onChange={onChange}
|
||||||
<option key={type} value={type}>
|
minWidth="100%"
|
||||||
{type}
|
disabled={isSubmitting}
|
||||||
</option>
|
options={Object.keys(SchemaType).map((type) => ({
|
||||||
))}
|
value: type,
|
||||||
</Select>
|
label: type,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<InputLabel>Compatibility level</InputLabel>
|
<InputLabel>Compatibility level</InputLabel>
|
||||||
<Select
|
<Controller
|
||||||
|
control={control}
|
||||||
name="compatibilityLevel"
|
name="compatibilityLevel"
|
||||||
defaultValue={schema.compatibilityLevel}
|
render={({ field: { name, onChange } }) => (
|
||||||
disabled={isSubmitting}
|
<Select
|
||||||
>
|
name={name}
|
||||||
{Object.keys(CompatibilityLevelCompatibilityEnum).map(
|
value={schema.compatibilityLevel}
|
||||||
(level: string) => (
|
onChange={onChange}
|
||||||
<option key={level} value={level}>
|
minWidth="100%"
|
||||||
{level}
|
disabled={isSubmitting}
|
||||||
</option>
|
options={Object.keys(
|
||||||
)
|
CompatibilityLevelCompatibilityEnum
|
||||||
|
).map((level) => ({ value: level, label: level }))}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Select>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<S.EditorsWrapper>
|
<S.EditorsWrapper>
|
||||||
|
|
|
@ -45,12 +45,8 @@ const GlobalSchemaSelector: React.FC = () => {
|
||||||
fetchData();
|
fetchData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleChangeCompatibilityLevel = (
|
const handleChangeCompatibilityLevel = (level: string | number) => {
|
||||||
event: React.ChangeEvent<HTMLSelectElement>
|
setNextCompatibilityLevel(level as CompatibilityLevelCompatibilityEnum);
|
||||||
) => {
|
|
||||||
setNextCompatibilityLevel(
|
|
||||||
event.target.value as CompatibilityLevelCompatibilityEnum
|
|
||||||
);
|
|
||||||
setIsConfirmationVisible(true);
|
setIsConfirmationVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -62,10 +58,10 @@ const GlobalSchemaSelector: React.FC = () => {
|
||||||
clusterName,
|
clusterName,
|
||||||
compatibilityLevel: { compatibility: nextCompatibilityLevel },
|
compatibilityLevel: { compatibility: nextCompatibilityLevel },
|
||||||
});
|
});
|
||||||
dispatch(fetchSchemas(clusterName));
|
|
||||||
setCurrentCompatibilityLevel(nextCompatibilityLevel);
|
setCurrentCompatibilityLevel(nextCompatibilityLevel);
|
||||||
setNextCompatibilityLevel(undefined);
|
setNextCompatibilityLevel(undefined);
|
||||||
setIsConfirmationVisible(false);
|
setIsConfirmationVisible(false);
|
||||||
|
dispatch(fetchSchemas(clusterName));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const err = await getResponse(e as Response);
|
const err = await getResponse(e as Response);
|
||||||
dispatch(serverErrorAlertAdded(err));
|
dispatch(serverErrorAlertAdded(err));
|
||||||
|
@ -81,18 +77,14 @@ const GlobalSchemaSelector: React.FC = () => {
|
||||||
<div>Global Compatibility Level: </div>
|
<div>Global Compatibility Level: </div>
|
||||||
<Select
|
<Select
|
||||||
selectSize="M"
|
selectSize="M"
|
||||||
value={currentCompatibilityLevel}
|
defaultValue={currentCompatibilityLevel}
|
||||||
|
minWidth="200px"
|
||||||
onChange={handleChangeCompatibilityLevel}
|
onChange={handleChangeCompatibilityLevel}
|
||||||
disabled={isFetching || isUpdating || isConfirmationVisible}
|
disabled={isFetching || isUpdating || isConfirmationVisible}
|
||||||
>
|
options={Object.keys(CompatibilityLevelCompatibilityEnum).map(
|
||||||
{Object.keys(CompatibilityLevelCompatibilityEnum).map(
|
(level) => ({ value: level, label: level })
|
||||||
(level: string) => (
|
|
||||||
<option key={level} value={level}>
|
|
||||||
{level}
|
|
||||||
</option>
|
|
||||||
)
|
|
||||||
)}
|
)}
|
||||||
</Select>
|
/>
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
isOpen={isConfirmationVisible}
|
isOpen={isConfirmationVisible}
|
||||||
onCancel={() => setIsConfirmationVisible(false)}
|
onCancel={() => setIsConfirmationVisible(false)}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { screen, waitFor } from '@testing-library/react';
|
import { screen, waitFor, within } from '@testing-library/react';
|
||||||
import { render } from 'lib/testHelpers';
|
import { render } from 'lib/testHelpers';
|
||||||
import { CompatibilityLevelCompatibilityEnum } from 'generated-sources';
|
import { CompatibilityLevelCompatibilityEnum } from 'generated-sources';
|
||||||
import GlobalSchemaSelector from 'components/Schemas/List/GlobalSchemaSelector/GlobalSchemaSelector';
|
import GlobalSchemaSelector from 'components/Schemas/List/GlobalSchemaSelector/GlobalSchemaSelector';
|
||||||
|
@ -10,15 +10,20 @@ import fetchMock from 'fetch-mock';
|
||||||
|
|
||||||
const clusterName = 'testClusterName';
|
const clusterName = 'testClusterName';
|
||||||
|
|
||||||
const selectForwardOption = () =>
|
const selectForwardOption = () => {
|
||||||
userEvent.selectOptions(
|
const dropdownElement = screen.getByRole('listbox');
|
||||||
screen.getByRole('listbox'),
|
// clicks to open dropdown
|
||||||
CompatibilityLevelCompatibilityEnum.FORWARD
|
userEvent.click(within(dropdownElement).getByRole('option'));
|
||||||
|
userEvent.click(
|
||||||
|
screen.getByText(CompatibilityLevelCompatibilityEnum.FORWARD)
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const expectOptionIsSelected = (option: string) => {
|
const expectOptionIsSelected = (option: string) => {
|
||||||
const optionElement: HTMLOptionElement = screen.getByText(option);
|
const dropdownElement = screen.getByRole('listbox');
|
||||||
expect(optionElement.selected).toBeTruthy();
|
const selectedOption = within(dropdownElement).getAllByRole('option');
|
||||||
|
expect(selectedOption.length).toEqual(1);
|
||||||
|
expect(selectedOption[0]).toHaveTextContent(option);
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('GlobalSchemaSelector', () => {
|
describe('GlobalSchemaSelector', () => {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { NewSchemaSubjectRaw } from 'redux/interfaces';
|
import { NewSchemaSubjectRaw } from 'redux/interfaces';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm, Controller } from 'react-hook-form';
|
||||||
import { ErrorMessage } from '@hookform/error-message';
|
import { ErrorMessage } from '@hookform/error-message';
|
||||||
import { clusterSchemaPath } from 'lib/paths';
|
import { clusterSchemaPath } from 'lib/paths';
|
||||||
import { SchemaType } from 'generated-sources';
|
import { SchemaType } from 'generated-sources';
|
||||||
|
@ -9,7 +9,7 @@ import { useHistory, useParams } from 'react-router';
|
||||||
import { InputLabel } from 'components/common/Input/InputLabel.styled';
|
import { InputLabel } from 'components/common/Input/InputLabel.styled';
|
||||||
import Input from 'components/common/Input/Input';
|
import Input from 'components/common/Input/Input';
|
||||||
import { FormError } from 'components/common/Input/Input.styled';
|
import { FormError } from 'components/common/Input/Input.styled';
|
||||||
import Select from 'components/common/Select/Select';
|
import Select, { SelectOption } from 'components/common/Select/Select';
|
||||||
import { Button } from 'components/common/Button/Button';
|
import { Button } from 'components/common/Button/Button';
|
||||||
import { Textarea } from 'components/common/Textbox/Textarea.styled';
|
import { Textarea } from 'components/common/Textbox/Textarea.styled';
|
||||||
import PageHeading from 'components/common/PageHeading/PageHeading';
|
import PageHeading from 'components/common/PageHeading/PageHeading';
|
||||||
|
@ -23,6 +23,12 @@ import { getResponse } from 'lib/errorHandling';
|
||||||
|
|
||||||
import * as S from './New.styled';
|
import * as S from './New.styled';
|
||||||
|
|
||||||
|
const SchemaTypeOptions: Array<SelectOption> = [
|
||||||
|
{ value: SchemaType.AVRO, label: 'AVRO' },
|
||||||
|
{ value: SchemaType.JSON, label: 'JSON' },
|
||||||
|
{ value: SchemaType.PROTOBUF, label: 'PROTOBUF' },
|
||||||
|
];
|
||||||
|
|
||||||
const New: React.FC = () => {
|
const New: React.FC = () => {
|
||||||
const { clusterName } = useParams<{ clusterName: string }>();
|
const { clusterName } = useParams<{ clusterName: string }>();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
@ -31,6 +37,7 @@ const New: React.FC = () => {
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
control,
|
||||||
formState: { isDirty, isSubmitting, errors },
|
formState: { isDirty, isSubmitting, errors },
|
||||||
} = methods;
|
} = methods;
|
||||||
|
|
||||||
|
@ -91,18 +98,22 @@ const New: React.FC = () => {
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<InputLabel>Schema Type *</InputLabel>
|
<InputLabel>Schema Type *</InputLabel>
|
||||||
<Select
|
<Controller
|
||||||
selectSize="M"
|
control={control}
|
||||||
|
rules={{ required: 'Schema Type is required.' }}
|
||||||
name="schemaType"
|
name="schemaType"
|
||||||
hookFormOptions={{
|
render={({ field: { name, onChange } }) => (
|
||||||
required: 'Schema Type is required.',
|
<Select
|
||||||
}}
|
selectSize="M"
|
||||||
disabled={isSubmitting}
|
name={name}
|
||||||
>
|
value={SchemaTypeOptions[0].value}
|
||||||
<option value={SchemaType.AVRO}>AVRO</option>
|
onChange={onChange}
|
||||||
<option value={SchemaType.JSON}>JSON</option>
|
minWidth="50%"
|
||||||
<option value={SchemaType.PROTOBUF}>PROTOBUF</option>
|
disabled={isSubmitting}
|
||||||
</Select>
|
options={SchemaTypeOptions}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FormError>
|
<FormError>
|
||||||
<ErrorMessage errors={errors} name="schemaType" />
|
<ErrorMessage errors={errors} name="schemaType" />
|
||||||
</FormError>
|
</FormError>
|
||||||
|
|
|
@ -11,10 +11,6 @@ export const FiltersWrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
|
|
||||||
& > div:last-child {
|
|
||||||
width: 10%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,15 @@ export interface FiltersProps {
|
||||||
|
|
||||||
const PER_PAGE = 100;
|
const PER_PAGE = 100;
|
||||||
|
|
||||||
|
const SeekTypeOptions = [
|
||||||
|
{ value: SeekType.OFFSET, label: 'Offset' },
|
||||||
|
{ value: SeekType.TIMESTAMP, label: 'Timestamp' },
|
||||||
|
];
|
||||||
|
const SeekDirectionOptions = [
|
||||||
|
{ value: SeekDirection.FORWARD, label: 'Oldest First' },
|
||||||
|
{ value: SeekDirection.BACKWARD, label: 'Newest First' },
|
||||||
|
];
|
||||||
|
|
||||||
const Filters: React.FC<FiltersProps> = ({
|
const Filters: React.FC<FiltersProps> = ({
|
||||||
clusterName,
|
clusterName,
|
||||||
topicName,
|
topicName,
|
||||||
|
@ -76,7 +85,7 @@ const Filters: React.FC<FiltersProps> = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
const [attempt, setAttempt] = React.useState(0);
|
const [attempt, setAttempt] = React.useState(0);
|
||||||
const [seekType, setSeekType] = React.useState<SeekType>(
|
const [currentSeekType, setCurrentSeekType] = React.useState<SeekType>(
|
||||||
(searchParams.get('seekType') as SeekType) || SeekType.OFFSET
|
(searchParams.get('seekType') as SeekType) || SeekType.OFFSET
|
||||||
);
|
);
|
||||||
const [offset, setOffset] = React.useState<string>(
|
const [offset, setOffset] = React.useState<string>(
|
||||||
|
@ -99,11 +108,11 @@ const Filters: React.FC<FiltersProps> = ({
|
||||||
|
|
||||||
const isSubmitDisabled = React.useMemo(() => {
|
const isSubmitDisabled = React.useMemo(() => {
|
||||||
if (isSeekTypeControlVisible) {
|
if (isSeekTypeControlVisible) {
|
||||||
return seekType === SeekType.TIMESTAMP && !timestamp;
|
return currentSeekType === SeekType.TIMESTAMP && !timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}, [isSeekTypeControlVisible, seekType, timestamp]);
|
}, [isSeekTypeControlVisible, currentSeekType, timestamp]);
|
||||||
|
|
||||||
const partitionMap = React.useMemo(
|
const partitionMap = React.useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
@ -128,11 +137,11 @@ const Filters: React.FC<FiltersProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isSeekTypeControlVisible) {
|
if (isSeekTypeControlVisible) {
|
||||||
props.seekType = seekType;
|
props.seekType = currentSeekType;
|
||||||
props.seekTo = selectedPartitions.map(({ value }) => {
|
props.seekTo = selectedPartitions.map(({ value }) => {
|
||||||
let seekToOffset;
|
let seekToOffset;
|
||||||
|
|
||||||
if (seekType === SeekType.OFFSET) {
|
if (currentSeekType === SeekType.OFFSET) {
|
||||||
if (offset) {
|
if (offset) {
|
||||||
seekToOffset = offset;
|
seekToOffset = offset;
|
||||||
} else {
|
} else {
|
||||||
|
@ -244,16 +253,13 @@ const Filters: React.FC<FiltersProps> = ({
|
||||||
<S.SeekTypeSelectorWrapper>
|
<S.SeekTypeSelectorWrapper>
|
||||||
<Select
|
<Select
|
||||||
id="selectSeekType"
|
id="selectSeekType"
|
||||||
onChange={({ target: { value } }) =>
|
onChange={(option) => setCurrentSeekType(option as SeekType)}
|
||||||
setSeekType(value as SeekType)
|
value={currentSeekType}
|
||||||
}
|
|
||||||
value={seekType}
|
|
||||||
selectSize="M"
|
selectSize="M"
|
||||||
>
|
minWidth="100px"
|
||||||
<option value={SeekType.OFFSET}>Offset</option>
|
options={SeekTypeOptions}
|
||||||
<option value={SeekType.TIMESTAMP}>Timestamp</option>
|
/>
|
||||||
</Select>
|
{currentSeekType === SeekType.OFFSET ? (
|
||||||
{seekType === SeekType.OFFSET ? (
|
|
||||||
<Input
|
<Input
|
||||||
id="offset"
|
id="offset"
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -311,13 +317,11 @@ const Filters: React.FC<FiltersProps> = ({
|
||||||
</S.FilterInputs>
|
</S.FilterInputs>
|
||||||
<Select
|
<Select
|
||||||
selectSize="M"
|
selectSize="M"
|
||||||
onChange={(e) => toggleSeekDirection(e.target.value)}
|
onChange={(option) => toggleSeekDirection(option as string)}
|
||||||
value={seekDirection}
|
value={seekDirection}
|
||||||
minWidth="120px"
|
minWidth="120px"
|
||||||
>
|
options={SeekDirectionOptions}
|
||||||
<option value={SeekDirection.FORWARD}>Oldest First</option>
|
/>
|
||||||
<option value={SeekDirection.BACKWARD}>Newest First</option>
|
|
||||||
</Select>
|
|
||||||
</div>
|
</div>
|
||||||
<S.FiltersMetrics>
|
<S.FiltersMetrics>
|
||||||
<p style={{ fontSize: 14 }}>{isFetching && phaseMessage}</p>
|
<p style={{ fontSize: 14 }}>{isFetching && phaseMessage}</p>
|
||||||
|
|
|
@ -181,48 +181,69 @@ exports[`Filters component matches the snapshot 1`] = `
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c7 {
|
.c6 {
|
||||||
|
position: relative;
|
||||||
|
list-style: none;
|
||||||
|
display: -webkit-box;
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-align-items: center;
|
||||||
|
-webkit-box-align: center;
|
||||||
|
-ms-flex-align: center;
|
||||||
|
align-items: center;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
border: 1px #ABB5BA solid;
|
border: 1px #ABB5BA solid;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
width: 100%;
|
width: -webkit-fit-content;
|
||||||
|
width: -moz-fit-content;
|
||||||
|
width: fit-content;
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
padding-right: 16px;
|
padding-right: 16px;
|
||||||
color: #171A1C;
|
color: #171A1C;
|
||||||
min-width: auto;
|
min-width: 100px;
|
||||||
background-image: url('data:image/svg+xml,%3Csvg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M1 1L5 5L9 1" stroke="%23454F54"/%3E%3C/svg%3E%0A') !important;
|
background-image: url('data:image/svg+xml,%3Csvg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M1 1L5 5L9 1" stroke="%23454F54"/%3E%3C/svg%3E%0A') !important;
|
||||||
background-repeat: no-repeat !important;
|
background-repeat: no-repeat !important;
|
||||||
background-position-x: calc(100% - 8px) !important;
|
background-position-x: calc(100% - 8px) !important;
|
||||||
background-position-y: 55% !important;
|
background-position-y: 55% !important;
|
||||||
-webkit-appearance: none !important;
|
|
||||||
-moz-appearance: none !important;
|
|
||||||
appearance: none !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.c7:hover {
|
.c6:hover {
|
||||||
color: #171A1C;
|
color: #171A1C;
|
||||||
border-color: #73848C;
|
border-color: #73848C;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c7:focus {
|
.c6:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
color: #171A1C;
|
color: #171A1C;
|
||||||
border-color: #454F54;
|
border-color: #454F54;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c7:disabled {
|
.c6:disabled {
|
||||||
color: #ABB5BA;
|
color: #ABB5BA;
|
||||||
border-color: #E3E6E8;
|
border-color: #E3E6E8;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c11 {
|
.c11 {
|
||||||
|
position: relative;
|
||||||
|
list-style: none;
|
||||||
|
display: -webkit-box;
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-align-items: center;
|
||||||
|
-webkit-box-align: center;
|
||||||
|
-ms-flex-align: center;
|
||||||
|
align-items: center;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
border: 1px #ABB5BA solid;
|
border: 1px #ABB5BA solid;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
width: 100%;
|
width: -webkit-fit-content;
|
||||||
|
width: -moz-fit-content;
|
||||||
|
width: fit-content;
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
padding-right: 16px;
|
padding-right: 16px;
|
||||||
color: #171A1C;
|
color: #171A1C;
|
||||||
|
@ -231,9 +252,6 @@ exports[`Filters component matches the snapshot 1`] = `
|
||||||
background-repeat: no-repeat !important;
|
background-repeat: no-repeat !important;
|
||||||
background-position-x: calc(100% - 8px) !important;
|
background-position-x: calc(100% - 8px) !important;
|
||||||
background-position-y: 55% !important;
|
background-position-y: 55% !important;
|
||||||
-webkit-appearance: none !important;
|
|
||||||
-moz-appearance: none !important;
|
|
||||||
appearance: none !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.c11:hover {
|
.c11:hover {
|
||||||
|
@ -253,8 +271,12 @@ exports[`Filters component matches the snapshot 1`] = `
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c6 {
|
.c7 {
|
||||||
position: relative;
|
padding-right: 16px;
|
||||||
|
list-style-position: inside;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c10 {
|
.c10 {
|
||||||
|
@ -332,10 +354,6 @@ exports[`Filters component matches the snapshot 1`] = `
|
||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c0 > div:first-child > div:last-child {
|
|
||||||
width: 10%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.c1 {
|
.c1 {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
display: -webkit-flex;
|
display: -webkit-flex;
|
||||||
|
@ -458,25 +476,19 @@ exports[`Filters component matches the snapshot 1`] = `
|
||||||
<div
|
<div
|
||||||
class="c5"
|
class="c5"
|
||||||
>
|
>
|
||||||
<div
|
<div>
|
||||||
class="select-wrapper c6"
|
<ul
|
||||||
>
|
class="c6"
|
||||||
<select
|
|
||||||
class="c7"
|
|
||||||
id="selectSeekType"
|
|
||||||
role="listbox"
|
role="listbox"
|
||||||
>
|
>
|
||||||
<option
|
<li
|
||||||
value="OFFSET"
|
class="c7"
|
||||||
|
role="option"
|
||||||
|
tabindex="0"
|
||||||
>
|
>
|
||||||
Offset
|
Offset
|
||||||
</option>
|
</li>
|
||||||
<option
|
</ul>
|
||||||
value="TIMESTAMP"
|
|
||||||
>
|
|
||||||
Timestamp
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="c2 offset-selector"
|
class="c2 offset-selector"
|
||||||
|
@ -558,24 +570,19 @@ exports[`Filters component matches the snapshot 1`] = `
|
||||||
Submit
|
Submit
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div>
|
||||||
class="select-wrapper c6"
|
<ul
|
||||||
>
|
|
||||||
<select
|
|
||||||
class="c11"
|
class="c11"
|
||||||
role="listbox"
|
role="listbox"
|
||||||
>
|
>
|
||||||
<option
|
<li
|
||||||
value="FORWARD"
|
class="c7"
|
||||||
|
role="option"
|
||||||
|
tabindex="0"
|
||||||
>
|
>
|
||||||
Oldest First
|
Oldest First
|
||||||
</option>
|
</li>
|
||||||
<option
|
</ul>
|
||||||
value="BACKWARD"
|
|
||||||
>
|
|
||||||
Newest First
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
@ -818,48 +825,69 @@ exports[`Filters component when fetching matches the snapshot 1`] = `
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c7 {
|
.c6 {
|
||||||
|
position: relative;
|
||||||
|
list-style: none;
|
||||||
|
display: -webkit-box;
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-align-items: center;
|
||||||
|
-webkit-box-align: center;
|
||||||
|
-ms-flex-align: center;
|
||||||
|
align-items: center;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
border: 1px #ABB5BA solid;
|
border: 1px #ABB5BA solid;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
width: 100%;
|
width: -webkit-fit-content;
|
||||||
|
width: -moz-fit-content;
|
||||||
|
width: fit-content;
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
padding-right: 16px;
|
padding-right: 16px;
|
||||||
color: #171A1C;
|
color: #171A1C;
|
||||||
min-width: auto;
|
min-width: 100px;
|
||||||
background-image: url('data:image/svg+xml,%3Csvg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M1 1L5 5L9 1" stroke="%23454F54"/%3E%3C/svg%3E%0A') !important;
|
background-image: url('data:image/svg+xml,%3Csvg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M1 1L5 5L9 1" stroke="%23454F54"/%3E%3C/svg%3E%0A') !important;
|
||||||
background-repeat: no-repeat !important;
|
background-repeat: no-repeat !important;
|
||||||
background-position-x: calc(100% - 8px) !important;
|
background-position-x: calc(100% - 8px) !important;
|
||||||
background-position-y: 55% !important;
|
background-position-y: 55% !important;
|
||||||
-webkit-appearance: none !important;
|
|
||||||
-moz-appearance: none !important;
|
|
||||||
appearance: none !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.c7:hover {
|
.c6:hover {
|
||||||
color: #171A1C;
|
color: #171A1C;
|
||||||
border-color: #73848C;
|
border-color: #73848C;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c7:focus {
|
.c6:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
color: #171A1C;
|
color: #171A1C;
|
||||||
border-color: #454F54;
|
border-color: #454F54;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c7:disabled {
|
.c6:disabled {
|
||||||
color: #ABB5BA;
|
color: #ABB5BA;
|
||||||
border-color: #E3E6E8;
|
border-color: #E3E6E8;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c11 {
|
.c11 {
|
||||||
|
position: relative;
|
||||||
|
list-style: none;
|
||||||
|
display: -webkit-box;
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-align-items: center;
|
||||||
|
-webkit-box-align: center;
|
||||||
|
-ms-flex-align: center;
|
||||||
|
align-items: center;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
border: 1px #ABB5BA solid;
|
border: 1px #ABB5BA solid;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
width: 100%;
|
width: -webkit-fit-content;
|
||||||
|
width: -moz-fit-content;
|
||||||
|
width: fit-content;
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
padding-right: 16px;
|
padding-right: 16px;
|
||||||
color: #171A1C;
|
color: #171A1C;
|
||||||
|
@ -868,9 +896,6 @@ exports[`Filters component when fetching matches the snapshot 1`] = `
|
||||||
background-repeat: no-repeat !important;
|
background-repeat: no-repeat !important;
|
||||||
background-position-x: calc(100% - 8px) !important;
|
background-position-x: calc(100% - 8px) !important;
|
||||||
background-position-y: 55% !important;
|
background-position-y: 55% !important;
|
||||||
-webkit-appearance: none !important;
|
|
||||||
-moz-appearance: none !important;
|
|
||||||
appearance: none !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.c11:hover {
|
.c11:hover {
|
||||||
|
@ -890,8 +915,12 @@ exports[`Filters component when fetching matches the snapshot 1`] = `
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c6 {
|
.c7 {
|
||||||
position: relative;
|
padding-right: 16px;
|
||||||
|
list-style-position: inside;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c10 {
|
.c10 {
|
||||||
|
@ -969,10 +998,6 @@ exports[`Filters component when fetching matches the snapshot 1`] = `
|
||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c0 > div:first-child > div:last-child {
|
|
||||||
width: 10%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.c1 {
|
.c1 {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
display: -webkit-flex;
|
display: -webkit-flex;
|
||||||
|
@ -1094,25 +1119,19 @@ exports[`Filters component when fetching matches the snapshot 1`] = `
|
||||||
<div
|
<div
|
||||||
class="c5"
|
class="c5"
|
||||||
>
|
>
|
||||||
<div
|
<div>
|
||||||
class="select-wrapper c6"
|
<ul
|
||||||
>
|
class="c6"
|
||||||
<select
|
|
||||||
class="c7"
|
|
||||||
id="selectSeekType"
|
|
||||||
role="listbox"
|
role="listbox"
|
||||||
>
|
>
|
||||||
<option
|
<li
|
||||||
value="OFFSET"
|
class="c7"
|
||||||
|
role="option"
|
||||||
|
tabindex="0"
|
||||||
>
|
>
|
||||||
Offset
|
Offset
|
||||||
</option>
|
</li>
|
||||||
<option
|
</ul>
|
||||||
value="TIMESTAMP"
|
|
||||||
>
|
|
||||||
Timestamp
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="c2 offset-selector"
|
class="c2 offset-selector"
|
||||||
|
@ -1194,24 +1213,19 @@ exports[`Filters component when fetching matches the snapshot 1`] = `
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div>
|
||||||
class="select-wrapper c6"
|
<ul
|
||||||
>
|
|
||||||
<select
|
|
||||||
class="c11"
|
class="c11"
|
||||||
role="listbox"
|
role="listbox"
|
||||||
>
|
>
|
||||||
<option
|
<li
|
||||||
value="FORWARD"
|
class="c7"
|
||||||
|
role="option"
|
||||||
|
tabindex="0"
|
||||||
>
|
>
|
||||||
Oldest First
|
Oldest First
|
||||||
</option>
|
</li>
|
||||||
<option
|
</ul>
|
||||||
value="BACKWARD"
|
|
||||||
>
|
|
||||||
Newest First
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -238,6 +238,11 @@ exports[`Details when it has readonly flag does not render the Action button a T
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"selectStyles": Object {
|
"selectStyles": Object {
|
||||||
|
"backgroundColor": Object {
|
||||||
|
"active": "#E3E6E8",
|
||||||
|
"hover": "#E3E6E8",
|
||||||
|
"normal": "#FFFFFF",
|
||||||
|
},
|
||||||
"borderColor": Object {
|
"borderColor": Object {
|
||||||
"active": "#454F54",
|
"active": "#454F54",
|
||||||
"disabled": "#E3E6E8",
|
"disabled": "#E3E6E8",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useRef } from 'react';
|
import React, { useRef } from 'react';
|
||||||
import { ErrorMessage } from '@hookform/error-message';
|
import { ErrorMessage } from '@hookform/error-message';
|
||||||
import { TOPIC_CUSTOM_PARAMS } from 'lib/constants';
|
import { TOPIC_CUSTOM_PARAMS } from 'lib/constants';
|
||||||
import { FieldArrayWithId, useFormContext } from 'react-hook-form';
|
import { FieldArrayWithId, useFormContext, Controller } from 'react-hook-form';
|
||||||
import { TopicFormData } from 'redux/interfaces';
|
import { TopicFormData } from 'redux/interfaces';
|
||||||
import { InputLabel } from 'components/common/Input/InputLabel.styled';
|
import { InputLabel } from 'components/common/Input/InputLabel.styled';
|
||||||
import { FormError } from 'components/common/Input/Input.styled';
|
import { FormError } from 'components/common/Input/Input.styled';
|
||||||
|
@ -34,6 +34,7 @@ const CustomParamField: React.FC<Props> = ({
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
setValue,
|
setValue,
|
||||||
watch,
|
watch,
|
||||||
|
control,
|
||||||
} = useFormContext<TopicFormData>();
|
} = useFormContext<TopicFormData>();
|
||||||
const nameValue = watch(`customParams.${index}.name`);
|
const nameValue = watch(`customParams.${index}.name`);
|
||||||
const prevName = useRef(nameValue);
|
const prevName = useRef(nameValue);
|
||||||
|
@ -49,7 +50,9 @@ const CustomParamField: React.FC<Props> = ({
|
||||||
prevName.current = nameValue;
|
prevName.current = nameValue;
|
||||||
newExistingFields.push(nameValue);
|
newExistingFields.push(nameValue);
|
||||||
setExistingFields(newExistingFields);
|
setExistingFields(newExistingFields);
|
||||||
setValue(`customParams.${index}.value`, TOPIC_CUSTOM_PARAMS[nameValue]);
|
setValue(`customParams.${index}.value`, TOPIC_CUSTOM_PARAMS[nameValue], {
|
||||||
|
shouldValidate: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [nameValue]);
|
}, [nameValue]);
|
||||||
|
|
||||||
|
@ -58,27 +61,27 @@ const CustomParamField: React.FC<Props> = ({
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<InputLabel>Custom Parameter</InputLabel>
|
<InputLabel>Custom Parameter</InputLabel>
|
||||||
<Select
|
<Controller
|
||||||
name={`customParams.${index}.name` as const}
|
control={control}
|
||||||
hookFormOptions={{
|
rules={{ required: 'Custom Parameter is required.' }}
|
||||||
required: 'Custom Parameter is required.',
|
name={`customParams.${index}.name`}
|
||||||
}}
|
render={({ field: { name, onChange } }) => (
|
||||||
disabled={isDisabled}
|
<Select
|
||||||
defaultValue={field.name}
|
name={name}
|
||||||
>
|
placeholder="Select"
|
||||||
<option value="">Select</option>
|
disabled={isDisabled}
|
||||||
{Object.keys(TOPIC_CUSTOM_PARAMS)
|
minWidth="270px"
|
||||||
.sort()
|
onChange={onChange}
|
||||||
.map((opt) => (
|
options={Object.keys(TOPIC_CUSTOM_PARAMS)
|
||||||
<option
|
.sort()
|
||||||
key={opt}
|
.map((opt) => ({
|
||||||
value={opt}
|
value: opt,
|
||||||
disabled={existingFields.includes(opt)}
|
label: opt,
|
||||||
>
|
disabled: existingFields.includes(opt),
|
||||||
{opt}
|
}))}
|
||||||
</option>
|
/>
|
||||||
))}
|
)}
|
||||||
</Select>
|
/>
|
||||||
<FormError>
|
<FormError>
|
||||||
<ErrorMessage
|
<ErrorMessage
|
||||||
errors={errors}
|
errors={errors}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { screen, within } from '@testing-library/react';
|
import {screen, waitFor, within} from '@testing-library/react';
|
||||||
import { render } from 'lib/testHelpers';
|
import { render } from 'lib/testHelpers';
|
||||||
import CustomParamsField, {
|
import CustomParamsField, {
|
||||||
Props,
|
Props,
|
||||||
|
@ -17,6 +17,11 @@ const setExistingFields = jest.fn();
|
||||||
|
|
||||||
const SPACE_KEY = ' ';
|
const SPACE_KEY = ' ';
|
||||||
|
|
||||||
|
const selectOption = async (listbox: HTMLElement, option: string) => {
|
||||||
|
await waitFor(() => userEvent.click(listbox));
|
||||||
|
await waitFor(() => userEvent.click(screen.getByText(option)));
|
||||||
|
};
|
||||||
|
|
||||||
describe('CustomParamsField', () => {
|
describe('CustomParamsField', () => {
|
||||||
const setupComponent = (props: Props) => {
|
const setupComponent = (props: Props) => {
|
||||||
const Wrapper: React.FC = ({ children }) => {
|
const Wrapper: React.FC = ({ children }) => {
|
||||||
|
@ -73,7 +78,7 @@ describe('CustomParamsField', () => {
|
||||||
expect(remove.mock.calls.length).toBe(2);
|
expect(remove.mock.calls.length).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can select option', () => {
|
it('can select option', async () => {
|
||||||
setupComponent({
|
setupComponent({
|
||||||
field,
|
field,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
|
@ -83,13 +88,14 @@ describe('CustomParamsField', () => {
|
||||||
setExistingFields,
|
setExistingFields,
|
||||||
});
|
});
|
||||||
const listbox = screen.getByRole('listbox');
|
const listbox = screen.getByRole('listbox');
|
||||||
userEvent.selectOptions(listbox, ['compression.type']);
|
await selectOption(listbox, 'compression.type');
|
||||||
|
|
||||||
const option = within(listbox).getByRole('option', { selected: true });
|
const selectedOption = within(listbox).getAllByRole('option');
|
||||||
expect(option).toHaveValue('compression.type');
|
expect(selectedOption.length).toEqual(1);
|
||||||
|
expect(selectedOption[0]).toHaveTextContent('compression.type');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('selecting option updates textbox value', () => {
|
it('selecting option updates textbox value', async () => {
|
||||||
setupComponent({
|
setupComponent({
|
||||||
field,
|
field,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
|
@ -99,13 +105,13 @@ describe('CustomParamsField', () => {
|
||||||
setExistingFields,
|
setExistingFields,
|
||||||
});
|
});
|
||||||
const listbox = screen.getByRole('listbox');
|
const listbox = screen.getByRole('listbox');
|
||||||
userEvent.selectOptions(listbox, ['compression.type']);
|
await selectOption(listbox, 'compression.type');
|
||||||
|
|
||||||
const textbox = screen.getByRole('textbox');
|
const textbox = screen.getByRole('textbox');
|
||||||
expect(textbox).toHaveValue(TOPIC_CUSTOM_PARAMS['compression.type']);
|
expect(textbox).toHaveValue(TOPIC_CUSTOM_PARAMS['compression.type']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('selecting option updates triggers setExistingFields', () => {
|
it('selecting option updates triggers setExistingFields', async () => {
|
||||||
setupComponent({
|
setupComponent({
|
||||||
field,
|
field,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
|
@ -115,7 +121,7 @@ describe('CustomParamsField', () => {
|
||||||
setExistingFields,
|
setExistingFields,
|
||||||
});
|
});
|
||||||
const listbox = screen.getByRole('listbox');
|
const listbox = screen.getByRole('listbox');
|
||||||
userEvent.selectOptions(listbox, ['compression.type']);
|
await selectOption(listbox, 'compression.type');
|
||||||
|
|
||||||
expect(setExistingFields.mock.calls.length).toBe(1);
|
expect(setExistingFields.mock.calls.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { screen, within } from '@testing-library/react';
|
import { screen, waitFor, within } from '@testing-library/react';
|
||||||
import { render } from 'lib/testHelpers';
|
import { render } from 'lib/testHelpers';
|
||||||
import CustomParams, {
|
import CustomParams, {
|
||||||
CustomParamsProps,
|
CustomParamsProps,
|
||||||
|
@ -8,6 +8,30 @@ import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { TOPIC_CUSTOM_PARAMS } from 'lib/constants';
|
import { TOPIC_CUSTOM_PARAMS } from 'lib/constants';
|
||||||
|
|
||||||
|
const selectOption = async (listbox: HTMLElement, option: string) => {
|
||||||
|
await waitFor(() => userEvent.click(listbox));
|
||||||
|
await waitFor(() => userEvent.click(screen.getByText(option)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectOptionIsSelected = (listbox: HTMLElement, option: string) => {
|
||||||
|
const selectedOption = within(listbox).getAllByRole('option');
|
||||||
|
expect(selectedOption.length).toEqual(1);
|
||||||
|
expect(selectedOption[0]).toHaveTextContent(option);
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectOptionIsDisabled = async (
|
||||||
|
listbox: HTMLElement,
|
||||||
|
option: string,
|
||||||
|
disabled: boolean
|
||||||
|
) => {
|
||||||
|
await waitFor(() => userEvent.click(listbox));
|
||||||
|
const selectedOption = within(listbox).getAllByText(option);
|
||||||
|
expect(selectedOption[1]).toHaveStyleRule(
|
||||||
|
'cursor',
|
||||||
|
disabled ? 'not-allowed' : 'pointer'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
describe('CustomParams', () => {
|
describe('CustomParams', () => {
|
||||||
const setupComponent = (props: CustomParamsProps) => {
|
const setupComponent = (props: CustomParamsProps) => {
|
||||||
const Wrapper: React.FC = ({ children }) => {
|
const Wrapper: React.FC = ({ children }) => {
|
||||||
|
@ -33,9 +57,9 @@ describe('CustomParams', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('works with user inputs correctly', () => {
|
describe('works with user inputs correctly', () => {
|
||||||
it('button click creates custom param fieldset', () => {
|
it('button click creates custom param fieldset', async () => {
|
||||||
const addParamButton = screen.getByRole('button');
|
const addParamButton = screen.getByRole('button');
|
||||||
userEvent.click(addParamButton);
|
await waitFor(() => userEvent.click(addParamButton));
|
||||||
|
|
||||||
const listbox = screen.getByRole('listbox');
|
const listbox = screen.getByRole('listbox');
|
||||||
expect(listbox).toBeInTheDocument();
|
expect(listbox).toBeInTheDocument();
|
||||||
|
@ -44,51 +68,39 @@ describe('CustomParams', () => {
|
||||||
expect(textbox).toBeInTheDocument();
|
expect(textbox).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can select option', () => {
|
it('can select option', async () => {
|
||||||
const addParamButton = screen.getByRole('button');
|
const addParamButton = screen.getByRole('button');
|
||||||
userEvent.click(addParamButton);
|
await waitFor(() => userEvent.click(addParamButton));
|
||||||
|
|
||||||
const listbox = screen.getByRole('listbox');
|
const listbox = screen.getByRole('listbox');
|
||||||
|
|
||||||
userEvent.selectOptions(listbox, ['compression.type']);
|
await selectOption(listbox, 'compression.type');
|
||||||
|
expectOptionIsSelected(listbox, 'compression.type');
|
||||||
const option = screen.getByRole('option', {
|
expectOptionIsDisabled(listbox, 'compression.type', true);
|
||||||
selected: true,
|
|
||||||
});
|
|
||||||
expect(option).toHaveValue('compression.type');
|
|
||||||
expect(option).toBeDisabled();
|
|
||||||
|
|
||||||
const textbox = screen.getByRole('textbox');
|
const textbox = screen.getByRole('textbox');
|
||||||
expect(textbox).toHaveValue(TOPIC_CUSTOM_PARAMS['compression.type']);
|
expect(textbox).toHaveValue(TOPIC_CUSTOM_PARAMS['compression.type']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('when selected option changes disabled options update correctly', () => {
|
it('when selected option changes disabled options update correctly', async () => {
|
||||||
const addParamButton = screen.getByRole('button');
|
const addParamButton = screen.getByRole('button');
|
||||||
userEvent.click(addParamButton);
|
await waitFor(() => userEvent.click(addParamButton));
|
||||||
|
|
||||||
const listbox = screen.getByRole('listbox');
|
const listbox = screen.getByRole('listbox');
|
||||||
|
|
||||||
userEvent.selectOptions(listbox, ['compression.type']);
|
await selectOption(listbox, 'compression.type');
|
||||||
|
expectOptionIsDisabled(listbox, 'compression.type', true);
|
||||||
|
|
||||||
const option = screen.getByRole('option', {
|
await selectOption(listbox, 'delete.retention.ms');
|
||||||
name: 'compression.type',
|
expectOptionIsDisabled(listbox, 'delete.retention.ms', true);
|
||||||
});
|
expectOptionIsDisabled(listbox, 'compression.type', false);
|
||||||
expect(option).toBeDisabled();
|
|
||||||
|
|
||||||
userEvent.selectOptions(listbox, ['delete.retention.ms']);
|
|
||||||
const newOption = screen.getByRole('option', {
|
|
||||||
name: 'delete.retention.ms',
|
|
||||||
});
|
|
||||||
expect(newOption).toBeDisabled();
|
|
||||||
|
|
||||||
expect(option).toBeEnabled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('multiple button clicks create multiple fieldsets', () => {
|
it('multiple button clicks create multiple fieldsets', async () => {
|
||||||
const addParamButton = screen.getByRole('button');
|
const addParamButton = screen.getByRole('button');
|
||||||
userEvent.click(addParamButton);
|
await waitFor(() => userEvent.click(addParamButton));
|
||||||
userEvent.click(addParamButton);
|
await waitFor(() => userEvent.click(addParamButton));
|
||||||
userEvent.click(addParamButton);
|
await waitFor(() => userEvent.click(addParamButton));
|
||||||
|
|
||||||
const listboxes = screen.getAllByRole('listbox');
|
const listboxes = screen.getAllByRole('listbox');
|
||||||
expect(listboxes.length).toBe(3);
|
expect(listboxes.length).toBe(3);
|
||||||
|
@ -97,7 +109,7 @@ describe('CustomParams', () => {
|
||||||
expect(textboxes.length).toBe(3);
|
expect(textboxes.length).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can't select already selected option", () => {
|
it("can't select already selected option", async () => {
|
||||||
const addParamButton = screen.getByRole('button');
|
const addParamButton = screen.getByRole('button');
|
||||||
userEvent.click(addParamButton);
|
userEvent.click(addParamButton);
|
||||||
userEvent.click(addParamButton);
|
userEvent.click(addParamButton);
|
||||||
|
@ -105,18 +117,11 @@ describe('CustomParams', () => {
|
||||||
const listboxes = screen.getAllByRole('listbox');
|
const listboxes = screen.getAllByRole('listbox');
|
||||||
|
|
||||||
const firstListbox = listboxes[0];
|
const firstListbox = listboxes[0];
|
||||||
userEvent.selectOptions(firstListbox, ['compression.type']);
|
await selectOption(firstListbox, 'compression.type');
|
||||||
|
expectOptionIsDisabled(firstListbox, 'compression.type', true);
|
||||||
const firstListboxOption = within(firstListbox).getByRole('option', {
|
|
||||||
selected: true,
|
|
||||||
});
|
|
||||||
expect(firstListboxOption).toBeDisabled();
|
|
||||||
|
|
||||||
const secondListbox = listboxes[1];
|
const secondListbox = listboxes[1];
|
||||||
const secondListboxOption = within(secondListbox).getByRole('option', {
|
expectOptionIsDisabled(secondListbox, 'compression.type', true);
|
||||||
name: 'compression.type',
|
|
||||||
});
|
|
||||||
expect(secondListboxOption).toBeDisabled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('when fieldset with selected custom property type is deleted disabled options update correctly', async () => {
|
it('when fieldset with selected custom property type is deleted disabled options update correctly', async () => {
|
||||||
|
@ -128,26 +133,16 @@ describe('CustomParams', () => {
|
||||||
const listboxes = screen.getAllByRole('listbox');
|
const listboxes = screen.getAllByRole('listbox');
|
||||||
|
|
||||||
const firstListbox = listboxes[0];
|
const firstListbox = listboxes[0];
|
||||||
userEvent.selectOptions(firstListbox, ['compression.type']);
|
await selectOption(firstListbox, 'compression.type');
|
||||||
|
expectOptionIsDisabled(firstListbox, 'compression.type', true);
|
||||||
const firstListboxOption = within(firstListbox).getByRole('option', {
|
|
||||||
selected: true,
|
|
||||||
});
|
|
||||||
expect(firstListboxOption).toBeDisabled();
|
|
||||||
|
|
||||||
const secondListbox = listboxes[1];
|
const secondListbox = listboxes[1];
|
||||||
userEvent.selectOptions(secondListbox, ['delete.retention.ms']);
|
await selectOption(secondListbox, 'delete.retention.ms');
|
||||||
const secondListboxOption = within(secondListbox).getByRole('option', {
|
expectOptionIsDisabled(secondListbox, 'delete.retention.ms', true);
|
||||||
selected: true,
|
|
||||||
});
|
|
||||||
expect(secondListboxOption).toBeDisabled();
|
|
||||||
|
|
||||||
const thirdListbox = listboxes[2];
|
const thirdListbox = listboxes[2];
|
||||||
userEvent.selectOptions(thirdListbox, ['file.delete.delay.ms']);
|
await selectOption(thirdListbox, 'file.delete.delay.ms');
|
||||||
const thirdListboxOption = within(thirdListbox).getByRole('option', {
|
expectOptionIsDisabled(thirdListbox, 'file.delete.delay.ms', true);
|
||||||
selected: true,
|
|
||||||
});
|
|
||||||
expect(thirdListboxOption).toBeDisabled();
|
|
||||||
|
|
||||||
const deleteSecondFieldsetButton = screen.getByTitle(
|
const deleteSecondFieldsetButton = screen.getByTitle(
|
||||||
'Delete customParam field 1'
|
'Delete customParam field 1'
|
||||||
|
@ -155,17 +150,8 @@ describe('CustomParams', () => {
|
||||||
userEvent.click(deleteSecondFieldsetButton);
|
userEvent.click(deleteSecondFieldsetButton);
|
||||||
expect(secondListbox).not.toBeInTheDocument();
|
expect(secondListbox).not.toBeInTheDocument();
|
||||||
|
|
||||||
expect(
|
expectOptionIsDisabled(firstListbox, 'delete.retention.ms', false);
|
||||||
within(firstListbox).getByRole('option', {
|
expectOptionIsDisabled(thirdListbox, 'delete.retention.ms', false);
|
||||||
name: 'delete.retention.ms',
|
|
||||||
})
|
|
||||||
).toBeEnabled();
|
|
||||||
|
|
||||||
expect(
|
|
||||||
within(thirdListbox).getByRole('option', {
|
|
||||||
name: 'delete.retention.ms',
|
|
||||||
})
|
|
||||||
).toBeEnabled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext, Controller } from 'react-hook-form';
|
||||||
import { BYTES_IN_GB } from 'lib/constants';
|
import { BYTES_IN_GB } from 'lib/constants';
|
||||||
import { TopicName, TopicConfigByName } from 'redux/interfaces';
|
import { TopicName, TopicConfigByName } from 'redux/interfaces';
|
||||||
import { ErrorMessage } from '@hookform/error-message';
|
import { ErrorMessage } from '@hookform/error-message';
|
||||||
import Select from 'components/common/Select/Select';
|
import Select, { SelectOption } from 'components/common/Select/Select';
|
||||||
import Input from 'components/common/Input/Input';
|
import Input from 'components/common/Input/Input';
|
||||||
import { Button } from 'components/common/Button/Button';
|
import { Button } from 'components/common/Button/Button';
|
||||||
import { InputLabel } from 'components/common/Input/InputLabel.styled';
|
import { InputLabel } from 'components/common/Input/InputLabel.styled';
|
||||||
import { FormError } from 'components/common/Input/Input.styled';
|
import { FormError } from 'components/common/Input/Input.styled';
|
||||||
import { StyledForm } from 'components/common/Form/Form.styles';
|
import { StyledForm } from 'components/common/Form/Form.styled';
|
||||||
|
|
||||||
import CustomParamsContainer from './CustomParams/CustomParamsContainer';
|
import CustomParamsContainer from './CustomParams/CustomParamsContainer';
|
||||||
import TimeToRetain from './TimeToRetain';
|
import TimeToRetain from './TimeToRetain';
|
||||||
|
@ -22,6 +22,20 @@ interface Props {
|
||||||
onSubmit: (e: React.BaseSyntheticEvent) => Promise<void>;
|
onSubmit: (e: React.BaseSyntheticEvent) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CleanupPolicyOptions: Array<SelectOption> = [
|
||||||
|
{ value: 'delete', label: 'Delete' },
|
||||||
|
{ value: 'compact', label: 'Compact' },
|
||||||
|
{ value: 'compact,delete', label: 'Compact,Delete' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const RetentionBytesOptions: Array<SelectOption> = [
|
||||||
|
{ value: -1, label: 'Not Set' },
|
||||||
|
{ value: BYTES_IN_GB, label: '1 GB' },
|
||||||
|
{ value: BYTES_IN_GB * 10, label: '10 GB' },
|
||||||
|
{ value: BYTES_IN_GB * 20, label: '20 GB' },
|
||||||
|
{ value: BYTES_IN_GB * 50, label: '50 GB' },
|
||||||
|
];
|
||||||
|
|
||||||
const TopicForm: React.FC<Props> = ({
|
const TopicForm: React.FC<Props> = ({
|
||||||
topicName,
|
topicName,
|
||||||
config,
|
config,
|
||||||
|
@ -30,6 +44,7 @@ const TopicForm: React.FC<Props> = ({
|
||||||
onSubmit,
|
onSubmit,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
|
control,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useFormContext();
|
} = useFormContext();
|
||||||
|
|
||||||
|
@ -99,11 +114,19 @@ const TopicForm: React.FC<Props> = ({
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<InputLabel>Cleanup policy</InputLabel>
|
<InputLabel>Cleanup policy</InputLabel>
|
||||||
<Select defaultValue="delete" name="cleanupPolicy" minWidth="250px">
|
<Controller
|
||||||
<option value="delete">Delete</option>
|
control={control}
|
||||||
<option value="compact">Compact</option>
|
name="cleanupPolicy"
|
||||||
<option value="compact,delete">Compact,Delete</option>
|
render={({ field: { name, onChange } }) => (
|
||||||
</Select>
|
<Select
|
||||||
|
name={name}
|
||||||
|
value={CleanupPolicyOptions[0].value}
|
||||||
|
onChange={onChange}
|
||||||
|
minWidth="250px"
|
||||||
|
options={CleanupPolicyOptions}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</S.Column>
|
</S.Column>
|
||||||
|
|
||||||
|
@ -116,13 +139,19 @@ const TopicForm: React.FC<Props> = ({
|
||||||
<S.Column>
|
<S.Column>
|
||||||
<div>
|
<div>
|
||||||
<InputLabel>Max size on disk in GB</InputLabel>
|
<InputLabel>Max size on disk in GB</InputLabel>
|
||||||
<Select defaultValue={-1} name="retentionBytes">
|
<Controller
|
||||||
<option value={-1}>Not Set</option>
|
control={control}
|
||||||
<option value={BYTES_IN_GB}>1 GB</option>
|
name="retentionBytes"
|
||||||
<option value={BYTES_IN_GB * 10}>10 GB</option>
|
render={({ field: { name, onChange } }) => (
|
||||||
<option value={BYTES_IN_GB * 20}>20 GB</option>
|
<Select
|
||||||
<option value={BYTES_IN_GB * 50}>50 GB</option>
|
name={name}
|
||||||
</Select>
|
value={RetentionBytesOptions[0].value}
|
||||||
|
onChange={onChange}
|
||||||
|
minWidth="100%"
|
||||||
|
options={RetentionBytesOptions}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -5,9 +5,9 @@ interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LiveIcon: React.FC<Props> = ({ className }) => {
|
const LiveIcon: React.FC<Props> = () => {
|
||||||
return (
|
return (
|
||||||
<i className={className}>
|
<i>
|
||||||
<svg
|
<svg
|
||||||
width="16"
|
width="16"
|
||||||
height="16"
|
height="16"
|
||||||
|
|
|
@ -4,23 +4,42 @@ interface Props {
|
||||||
selectSize: 'M' | 'L';
|
selectSize: 'M' | 'L';
|
||||||
isLive?: boolean;
|
isLive?: boolean;
|
||||||
minWidth?: string;
|
minWidth?: string;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Select = styled.select<Props>`
|
interface OptionProps {
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Select = styled.ul<Props>`
|
||||||
|
position: relative;
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
height: ${(props) => (props.selectSize === 'M' ? '32px' : '40px')};
|
height: ${(props) => (props.selectSize === 'M' ? '32px' : '40px')};
|
||||||
border: 1px ${(props) => props.theme.selectStyles.borderColor.normal} solid;
|
border: 1px
|
||||||
|
${({ theme, disabled }) =>
|
||||||
|
disabled
|
||||||
|
? theme.selectStyles.borderColor.disabled
|
||||||
|
: theme.selectStyles.borderColor.normal}
|
||||||
|
solid;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
width: 100%;
|
width: fit-content;
|
||||||
padding-left: ${(props) => (props.isLive ? '36px' : '12px')};
|
padding-left: ${(props) => (props.isLive ? '36px' : '12px')};
|
||||||
padding-right: 16px;
|
padding-right: 16px;
|
||||||
color: ${(props) => props.theme.selectStyles.color.normal};
|
color: ${({ theme, disabled }) =>
|
||||||
|
disabled
|
||||||
|
? theme.selectStyles.color.disabled
|
||||||
|
: theme.selectStyles.color.normal};
|
||||||
min-width: ${({ minWidth }) => minWidth || 'auto'};
|
min-width: ${({ minWidth }) => minWidth || 'auto'};
|
||||||
background-image: url('data:image/svg+xml,%3Csvg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M1 1L5 5L9 1" stroke="%23454F54"/%3E%3C/svg%3E%0A') !important;
|
background-image: ${({ disabled }) =>
|
||||||
|
`url('data:image/svg+xml,%3Csvg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M1 1L5 5L9 1" stroke="${
|
||||||
|
disabled ? '%23ABB5BA' : '%23454F54'
|
||||||
|
}"/%3E%3C/svg%3E%0A') !important`};
|
||||||
background-repeat: no-repeat !important;
|
background-repeat: no-repeat !important;
|
||||||
background-position-x: calc(100% - 8px) !important;
|
background-position-x: calc(100% - 8px) !important;
|
||||||
background-position-y: 55% !important;
|
background-position-y: 55% !important;
|
||||||
appearance: none !important;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: ${(props) => props.theme.selectStyles.color.hover};
|
color: ${(props) => props.theme.selectStyles.color.hover};
|
||||||
|
@ -37,3 +56,46 @@ export const Select = styled.select<Props>`
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const OptionList = styled.ul`
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
max-height: 114px;
|
||||||
|
margin-top: 4px;
|
||||||
|
background-color: ${(props) =>
|
||||||
|
props.theme.selectStyles.backgroundColor.normal};
|
||||||
|
border: 1px ${(props) => props.theme.selectStyles.borderColor.normal} solid;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 18px;
|
||||||
|
width: 100%;
|
||||||
|
color: ${(props) => props.theme.selectStyles.color.normal};
|
||||||
|
overflow-y: scroll;
|
||||||
|
z-index: 10;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Option = styled.li<OptionProps>`
|
||||||
|
list-style: none;
|
||||||
|
padding: 10px 12px;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: ${(props) =>
|
||||||
|
props.theme.selectStyles.backgroundColor.hover};
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: ${(props) =>
|
||||||
|
props.theme.selectStyles.backgroundColor.active};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SelectedOption = styled.li`
|
||||||
|
padding-right: 16px;
|
||||||
|
list-style-position: inside;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
`;
|
||||||
|
|
|
@ -1,56 +1,96 @@
|
||||||
import styled from 'styled-components';
|
import React, { useState, useRef } from 'react';
|
||||||
import React from 'react';
|
import useClickOutside from 'lib/hooks/useClickOutside';
|
||||||
import { RegisterOptions, useFormContext } from 'react-hook-form';
|
|
||||||
|
|
||||||
import LiveIcon from './LiveIcon.styled';
|
|
||||||
import * as S from './Select.styled';
|
import * as S from './Select.styled';
|
||||||
|
import LiveIcon from './LiveIcon.styled';
|
||||||
|
|
||||||
export interface SelectProps
|
export interface SelectProps {
|
||||||
extends React.SelectHTMLAttributes<HTMLSelectElement> {
|
options?: Array<SelectOption>;
|
||||||
|
id?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
selectSize?: 'M' | 'L';
|
selectSize?: 'M' | 'L';
|
||||||
isLive?: boolean;
|
isLive?: boolean;
|
||||||
hookFormOptions?: RegisterOptions;
|
|
||||||
minWidth?: string;
|
minWidth?: string;
|
||||||
|
value?: string | number;
|
||||||
|
defaultValue?: string | number;
|
||||||
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
onChange?: (option: string | number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectOption {
|
||||||
|
label: string | number;
|
||||||
|
value: string | number;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Select: React.FC<SelectProps> = ({
|
const Select: React.FC<SelectProps> = ({
|
||||||
className,
|
id,
|
||||||
children,
|
options = [],
|
||||||
|
value,
|
||||||
|
defaultValue,
|
||||||
selectSize = 'L',
|
selectSize = 'L',
|
||||||
|
placeholder = '',
|
||||||
isLive,
|
isLive,
|
||||||
name,
|
disabled = false,
|
||||||
hookFormOptions,
|
onChange,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const methods = useFormContext();
|
const [selectedOption, setSelectedOption] = useState(value);
|
||||||
|
const [showOptions, setShowOptions] = useState(false);
|
||||||
|
|
||||||
|
const showOptionsHandler = () => {
|
||||||
|
if (!disabled) setShowOptions(!showOptions);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectContainerRef = useRef(null);
|
||||||
|
const clickOutsideHandler = () => setShowOptions(false);
|
||||||
|
useClickOutside(selectContainerRef, clickOutsideHandler);
|
||||||
|
|
||||||
|
const updateSelectedOption = (option: SelectOption) => {
|
||||||
|
if (disabled) return;
|
||||||
|
|
||||||
|
setSelectedOption(option.value);
|
||||||
|
if (onChange) onChange(option.value);
|
||||||
|
setShowOptions(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`select-wrapper ${className}`}>
|
<div ref={selectContainerRef}>
|
||||||
{isLive && <LiveIcon />}
|
{isLive && <LiveIcon />}
|
||||||
{name ? (
|
<S.Select
|
||||||
<S.Select
|
role="listbox"
|
||||||
role="listbox"
|
selectSize={selectSize}
|
||||||
selectSize={selectSize}
|
isLive={isLive}
|
||||||
isLive={isLive}
|
disabled={disabled}
|
||||||
{...methods.register(name, { ...hookFormOptions })}
|
onClick={showOptionsHandler}
|
||||||
{...props}
|
onKeyDown={showOptionsHandler}
|
||||||
>
|
{...props}
|
||||||
{children}
|
>
|
||||||
</S.Select>
|
<S.SelectedOption role="option" tabIndex={0}>
|
||||||
) : (
|
{options.find(
|
||||||
<S.Select
|
(option) => option.value === (defaultValue || selectedOption)
|
||||||
role="listbox"
|
)?.label || placeholder}
|
||||||
selectSize={selectSize}
|
</S.SelectedOption>
|
||||||
isLive={isLive}
|
{showOptions && (
|
||||||
{...props}
|
<S.OptionList>
|
||||||
>
|
{options?.map((option) => (
|
||||||
{children}
|
<S.Option
|
||||||
</S.Select>
|
value={option.value}
|
||||||
)}
|
key={option.value}
|
||||||
|
disabled={option.disabled}
|
||||||
|
onClick={() => updateSelectedOption(option)}
|
||||||
|
tabIndex={0}
|
||||||
|
role="option"
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</S.Option>
|
||||||
|
))}
|
||||||
|
</S.OptionList>
|
||||||
|
)}
|
||||||
|
</S.Select>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default styled(Select)`
|
export default Select;
|
||||||
position: relative;
|
|
||||||
`;
|
|
||||||
|
|
|
@ -2,22 +2,24 @@
|
||||||
|
|
||||||
exports[`Custom Select when live matches the snapshot 1`] = `
|
exports[`Custom Select when live matches the snapshot 1`] = `
|
||||||
<body>
|
<body>
|
||||||
.c1 {
|
.c0 {
|
||||||
position: absolute;
|
position: relative;
|
||||||
left: 12px;
|
list-style: none;
|
||||||
top: 50%;
|
display: -webkit-box;
|
||||||
-webkit-transform: translateY(-50%);
|
display: -webkit-flex;
|
||||||
-ms-transform: translateY(-50%);
|
display: -ms-flexbox;
|
||||||
transform: translateY(-50%);
|
display: flex;
|
||||||
line-height: 0;
|
-webkit-align-items: center;
|
||||||
}
|
-webkit-box-align: center;
|
||||||
|
-ms-flex-align: center;
|
||||||
.c2 {
|
align-items: center;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
border: 1px #ABB5BA solid;
|
border: 1px #ABB5BA solid;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
width: 100%;
|
width: -webkit-fit-content;
|
||||||
|
width: -moz-fit-content;
|
||||||
|
width: fit-content;
|
||||||
padding-left: 36px;
|
padding-left: 36px;
|
||||||
padding-right: 16px;
|
padding-right: 16px;
|
||||||
color: #171A1C;
|
color: #171A1C;
|
||||||
|
@ -26,39 +28,36 @@ exports[`Custom Select when live matches the snapshot 1`] = `
|
||||||
background-repeat: no-repeat !important;
|
background-repeat: no-repeat !important;
|
||||||
background-position-x: calc(100% - 8px) !important;
|
background-position-x: calc(100% - 8px) !important;
|
||||||
background-position-y: 55% !important;
|
background-position-y: 55% !important;
|
||||||
-webkit-appearance: none !important;
|
|
||||||
-moz-appearance: none !important;
|
|
||||||
appearance: none !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.c2:hover {
|
.c0:hover {
|
||||||
color: #171A1C;
|
color: #171A1C;
|
||||||
border-color: #73848C;
|
border-color: #73848C;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c2:focus {
|
.c0:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
color: #171A1C;
|
color: #171A1C;
|
||||||
border-color: #454F54;
|
border-color: #454F54;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c2:disabled {
|
.c0:disabled {
|
||||||
color: #ABB5BA;
|
color: #ABB5BA;
|
||||||
border-color: #E3E6E8;
|
border-color: #E3E6E8;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c0 {
|
.c1 {
|
||||||
position: relative;
|
padding-right: 16px;
|
||||||
|
list-style-position: inside;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div>
|
||||||
class="select-wrapper c0"
|
<i>
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="c1"
|
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
fill="none"
|
fill="none"
|
||||||
height="16"
|
height="16"
|
||||||
|
@ -80,22 +79,41 @@ exports[`Custom Select when live matches the snapshot 1`] = `
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</i>
|
</i>
|
||||||
<select
|
<ul
|
||||||
class="c2"
|
class="c0"
|
||||||
|
name="test"
|
||||||
role="listbox"
|
role="listbox"
|
||||||
/>
|
>
|
||||||
|
<li
|
||||||
|
class="c1"
|
||||||
|
role="option"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Custom Select when non-live matches the snapshot 1`] = `
|
exports[`Custom Select when non-live matches the snapshot 1`] = `
|
||||||
.c1 {
|
.c0 {
|
||||||
|
position: relative;
|
||||||
|
list-style: none;
|
||||||
|
display: -webkit-box;
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-align-items: center;
|
||||||
|
-webkit-box-align: center;
|
||||||
|
-ms-flex-align: center;
|
||||||
|
align-items: center;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
border: 1px #ABB5BA solid;
|
border: 1px #ABB5BA solid;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
width: 100%;
|
width: -webkit-fit-content;
|
||||||
|
width: -moz-fit-content;
|
||||||
|
width: fit-content;
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
padding-right: 16px;
|
padding-right: 16px;
|
||||||
color: #171A1C;
|
color: #171A1C;
|
||||||
|
@ -104,41 +122,47 @@ exports[`Custom Select when non-live matches the snapshot 1`] = `
|
||||||
background-repeat: no-repeat !important;
|
background-repeat: no-repeat !important;
|
||||||
background-position-x: calc(100% - 8px) !important;
|
background-position-x: calc(100% - 8px) !important;
|
||||||
background-position-y: 55% !important;
|
background-position-y: 55% !important;
|
||||||
-webkit-appearance: none !important;
|
|
||||||
-moz-appearance: none !important;
|
|
||||||
appearance: none !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.c1:hover {
|
.c0:hover {
|
||||||
color: #171A1C;
|
color: #171A1C;
|
||||||
border-color: #73848C;
|
border-color: #73848C;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c1:focus {
|
.c0:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
color: #171A1C;
|
color: #171A1C;
|
||||||
border-color: #454F54;
|
border-color: #454F54;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c1:disabled {
|
.c0:disabled {
|
||||||
color: #ABB5BA;
|
color: #ABB5BA;
|
||||||
border-color: #E3E6E8;
|
border-color: #E3E6E8;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c0 {
|
.c1 {
|
||||||
position: relative;
|
padding-right: 16px;
|
||||||
|
list-style-position: inside;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div>
|
||||||
class="select-wrapper c0"
|
<ul
|
||||||
>
|
class="c0"
|
||||||
<select
|
name="test"
|
||||||
class="c1"
|
|
||||||
role="listbox"
|
role="listbox"
|
||||||
/>
|
>
|
||||||
|
<li
|
||||||
|
class="c1"
|
||||||
|
role="option"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
29
kafka-ui-react-app/src/lib/hooks/useClickOutside.ts
Normal file
29
kafka-ui-react-app/src/lib/hooks/useClickOutside.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { RefObject, useEffect } from 'react';
|
||||||
|
|
||||||
|
type Event = MouseEvent | TouchEvent;
|
||||||
|
|
||||||
|
const useClickOutside = <T extends HTMLElement = HTMLElement>(
|
||||||
|
ref: RefObject<T>,
|
||||||
|
handler: (event: Event) => void
|
||||||
|
) => {
|
||||||
|
useEffect(() => {
|
||||||
|
const listener = (event: Event) => {
|
||||||
|
const el = ref?.current;
|
||||||
|
if (!el || el.contains((event?.target as Node) || null)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handler(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', listener);
|
||||||
|
document.addEventListener('touchstart', listener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', listener);
|
||||||
|
document.removeEventListener('touchstart', listener);
|
||||||
|
};
|
||||||
|
}, [ref, handler]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useClickOutside;
|
|
@ -165,6 +165,11 @@ const theme = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
selectStyles: {
|
selectStyles: {
|
||||||
|
backgroundColor: {
|
||||||
|
normal: Colors.neutral[0],
|
||||||
|
hover: Colors.neutral[10],
|
||||||
|
active: Colors.neutral[10],
|
||||||
|
},
|
||||||
color: {
|
color: {
|
||||||
normal: Colors.neutral[90],
|
normal: Colors.neutral[90],
|
||||||
hover: Colors.neutral[90],
|
hover: Colors.neutral[90],
|
||||||
|
|
Loading…
Add table
Reference in a new issue