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