[Lens] don't allow to drag and drop a single element (#141793)

* [wip][Lens] don't allow to drag and drop a single element

* do some cleanup

* push some changes

* update field_inputs

* some cleanup

* fix styles

* push some code

* push some changes

* fix some tests

* push some changes

* add padding

* fix styles

* first attempt at cleaning up markup and styles

* change the name of color to bgColor, pass isDragging state

* Update default_bucket_container.tsx

more overlooked copy

* fix JEST

* Update x-pack/plugins/lens/public/shared_components/drag_drop_bucket/fields_bucket_container.tsx

Co-authored-by: Michael Marcialis <michael@marcial.is>

* Update x-pack/plugins/lens/public/shared_components/drag_drop_bucket/default_bucket_container.tsx

Co-authored-by: Michael Marcialis <michael@marcial.is>

* Update x-pack/plugins/lens/public/shared_components/drag_drop_bucket/default_bucket_container.tsx

Co-authored-by: Michael Marcialis <michael@marcial.is>

* Update x-pack/plugins/lens/public/shared_components/drag_drop_bucket/default_bucket_container.tsx

Co-authored-by: Michael Marcialis <michael@marcial.is>

* Update x-pack/plugins/lens/public/shared_components/drag_drop_bucket/default_bucket_container.tsx

Co-authored-by: Michael Marcialis <michael@marcial.is>

* Update x-pack/plugins/lens/public/shared_components/drag_drop_bucket/buckets.tsx

Co-authored-by: Michael Marcialis <michael@marcial.is>

* Update x-pack/plugins/lens/public/shared_components/drag_drop_bucket/buckets.tsx

Co-authored-by: Michael Marcialis <michael@marcial.is>

* Update x-pack/plugins/lens/public/shared_components/drag_drop_bucket/fields_bucket_container.tsx

Co-authored-by: Michael Marcialis <michael@marcial.is>

* Update x-pack/plugins/lens/public/shared_components/drag_drop_bucket/fields_bucket_container.tsx

Co-authored-by: Michael Marcialis <michael@marcial.is>

* Update x-pack/plugins/lens/public/shared_components/drag_drop_bucket/default_bucket_container.tsx

Co-authored-by: Michael Marcialis <michael@marcial.is>

* fix JEST

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Marta Bondyra <4283304+mbondyra@users.noreply.github.com>
Co-authored-by: Michael Marcialis <michael.marcialis@elastic.co>
Co-authored-by: Marta Bondyra <marta.bondyra@elastic.co>
Co-authored-by: Michael Marcialis <michael@marcial.is>
This commit is contained in:
Alexey Antonov 2022-09-30 16:48:09 +03:00 committed by GitHub
parent 6157f0be86
commit 067484b9b3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 494 additions and 437 deletions

View file

@ -399,8 +399,8 @@ describe('filters', () => {
);
instance
.find('[data-test-subj="lns-customBucketContainer-remove"]')
.at(2)
.find('[data-test-subj="lns-customBucketContainer-remove-1"]')
.at(0)
.simulate('click');
expect(updateLayerSpy).toHaveBeenCalledWith({
...layer,

View file

@ -235,13 +235,14 @@ export const FilterList = ({
droppableId="FILTERS_DROPPABLE_AREA"
items={localFilters}
>
{localFilters?.map((filter: FilterValue, idx: number) => {
{localFilters?.map((filter, idx, arrayRef) => {
const isInvalid = !isQueryValid(filter.input, indexPattern);
const id = filter.id;
return (
<DraggableBucketContainer
id={filter.id}
key={filter.id}
id={id}
key={id}
idx={idx}
isInvalid={isInvalid}
invalidMessage={i18n.translate('xpack.lens.indexPattern.filters.isInvalid', {
@ -251,7 +252,8 @@ export const FilterList = ({
removeTitle={i18n.translate('xpack.lens.indexPattern.filters.removeFilter', {
defaultMessage: 'Remove a filter',
})}
isNotRemovable={localFilters.length === 1}
isNotRemovable={arrayRef.length === 1}
isNotDraggable={arrayRef.length === 1}
>
<FilterPopover
data-test-subj="indexPattern-filters-existingFilterContainer"

View file

@ -7,7 +7,7 @@
import './advanced_editor.scss';
import React, { useState } from 'react';
import React, { useState, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiFlexGroup,
@ -220,7 +220,7 @@ export const AdvancedRangeEditor = ({
[localRanges]
);
const addNewRange = () => {
const addNewRange = useCallback(() => {
const newRangeId = generateId();
setLocalRanges([
@ -228,13 +228,13 @@ export const AdvancedRangeEditor = ({
{
id: newRangeId,
from: localRanges[localRanges.length - 1].to,
to: Infinity,
to: Number.POSITIVE_INFINITY,
label: '',
},
]);
setActiveRangeId(newRangeId);
};
}, [localRanges]);
const changeActiveRange = (rangeId: string) => {
let newActiveRangeId = rangeId;
@ -264,11 +264,10 @@ export const AdvancedRangeEditor = ({
<>
<DragDropBuckets
onDragEnd={setLocalRanges}
onDragStart={() => {}}
droppableId="RANGES_DROPPABLE_AREA"
items={localRanges}
>
{localRanges.map((range: LocalRangeType, idx: number) => (
{localRanges.map((range, idx, arrayRef) => (
<DraggableBucketContainer
key={range.id}
idx={idx}
@ -278,20 +277,21 @@ export const AdvancedRangeEditor = ({
defaultMessage: 'This range is invalid',
})}
onRemoveClick={() => {
const newRanges = localRanges.filter((_, i) => i !== idx);
const newRanges = arrayRef.filter((_, i) => i !== idx);
setLocalRanges(newRanges);
}}
removeTitle={i18n.translate('xpack.lens.indexPattern.ranges.deleteRange', {
defaultMessage: 'Delete range',
})}
isNotRemovable={localRanges.length === 1}
isNotRemovable={arrayRef.length === 1}
isNotDraggable={arrayRef.length < 2}
>
<RangePopover
range={range}
isOpen={range.id === activeRangeId}
triggerClose={() => changeActiveRange('')}
setRange={(newRange: LocalRangeType) => {
const newRanges = [...localRanges];
const newRanges = [...arrayRef];
if (newRange.id === newRanges[idx].id) {
newRanges[idx] = newRange;
} else {
@ -320,9 +320,7 @@ export const AdvancedRangeEditor = ({
))}
</DragDropBuckets>
<NewBucketButton
onClick={() => {
addNewRange();
}}
onClick={addNewRange}
label={i18n.translate('xpack.lens.indexPattern.ranges.addRange', {
defaultMessage: 'Add range',
})}

View file

@ -797,8 +797,8 @@ describe('ranges', () => {
// This series of act closures are made to make it work properly the update flush
act(() => {
instance
.find('[data-test-subj="lns-customBucketContainer-remove"]')
.last()
.find('[data-test-subj="lns-customBucketContainer-remove-1"]')
.at(0)
.simulate('click');
});

View file

@ -5,24 +5,16 @@
* 2.0.
*/
import React, { useCallback, useMemo, useState } from 'react';
import {
EuiButtonIcon,
EuiDraggable,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
htmlIdGenerator,
EuiPanel,
useEuiTheme,
} from '@elastic/eui';
import React, { useCallback, useMemo } from 'react';
import { htmlIdGenerator } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { ExistingFieldsMap, IndexPattern } from '../../../../types';
import {
DragDropBuckets,
FieldsBucketContainer,
NewBucketButton,
TooltipWrapper,
useDebouncedValue,
DraggableBucketContainer,
} from '../../../../shared_components';
import { FieldSelect } from '../../../dimension_panel/field_select';
import type { TermsIndexPatternColumn } from './types';
@ -61,9 +53,6 @@ export function FieldInputs({
operationSupportMatrix,
invalidFields,
}: FieldInputsProps) {
const { euiTheme } = useEuiTheme();
const [isDragging, setIsDragging] = useState(false);
const onChangeWrapped = useCallback(
(values: WrappedValue[]) =>
onChange(values.filter(removeNewEmptyField).map(({ value }) => value)),
@ -97,154 +86,90 @@ export function FieldInputs({
);
const disableActions =
(localValues.length === 2 && localValues.some(({ isNew }) => isNew)) ||
localValues.length === 1;
localValues.length === 1 || localValues.filter(({ isNew }) => !isNew).length < 2;
const localValuesFilled = localValues.filter(({ isNew }) => !isNew);
return (
<>
<div
style={{
backgroundColor: isDragging ? 'transparent' : euiTheme.colors.lightestShade,
borderRadius: euiTheme.size.xs,
marginBottom: euiTheme.size.xs,
<DragDropBuckets
onDragEnd={(updatedValues: WrappedValue[]) => {
handleInputChange(updatedValues);
}}
droppableId="TOP_TERMS_DROPPABLE_AREA"
items={localValues}
bgColor="subdued"
>
<DragDropBuckets
onDragEnd={(updatedValues: WrappedValue[]) => {
handleInputChange(updatedValues);
setIsDragging(false);
}}
className="lnsIndexPatternDimensionEditor__droppable"
onDragStart={() => {
setIsDragging(true);
}}
droppableId="TOP_TERMS_DROPPABLE_AREA"
items={localValues}
>
{localValues.map(({ id, value, isNew }, index) => {
// need to filter the available fields for multiple terms
// * a scripted field should be removed
// * a field of unsupported type should be removed
// * a field that has been used
// * a scripted field was used in a singular term, should be marked as invalid for multi-terms
const filteredOperationByField = Object.keys(operationSupportMatrix.operationByField)
.filter((key) => {
if (key === value) {
return true;
}
const field = indexPattern.getFieldByName(key);
if (index === 0) {
return !rawValuesLookup.has(key) && field && supportedTypes.has(field.type);
} else {
return (
!rawValuesLookup.has(key) &&
field &&
!field.scripted &&
supportedTypes.has(field.type)
);
}
})
.reduce<OperationSupportMatrix['operationByField']>((memo, key) => {
memo[key] = operationSupportMatrix.operationByField[key];
return memo;
}, {});
{localValues.map(({ id, value, isNew }, index, arrayRef) => {
// need to filter the available fields for multiple terms
// * a scripted field should be removed
// * a field of unsupported type should be removed
// * a field that has been used
// * a scripted field was used in a singular term, should be marked as invalid for multi-terms
const filteredOperationByField = Object.keys(operationSupportMatrix.operationByField)
.filter((key) => {
if (key === value) {
return true;
}
const field = indexPattern.getFieldByName(key);
if (index === 0) {
return !rawValuesLookup.has(key) && field && supportedTypes.has(field.type);
} else {
return (
!rawValuesLookup.has(key) &&
field &&
!field.scripted &&
supportedTypes.has(field.type)
);
}
})
.reduce<OperationSupportMatrix['operationByField']>((memo, key) => {
memo[key] = operationSupportMatrix.operationByField[key];
return memo;
}, {});
const shouldShowError = Boolean(
value &&
((indexPattern.getFieldByName(value)?.scripted && localValuesFilled.length > 1) ||
invalidFields?.includes(value))
);
return (
<EuiDraggable
spacing="none"
index={index}
draggableId={value || 'newField'}
key={id}
disableInteractiveElementBlocking
>
{(provided) => (
<EuiPanel paddingSize="xs" hasShadow={false} color="transparent">
<EuiFlexGroup gutterSize="none" alignItems="center" responsive={false}>
<EuiFlexItem
grow={false}
className="lnsIndexPatternDimensionEditor__droppableItem"
>
<EuiIcon
size="s"
color="text"
type="grab"
title={i18n.translate('xpack.lens.indexPattern.terms.dragToReorder', {
defaultMessage: 'Drag to reorder',
})}
data-test-subj={`indexPattern-terms-dragToReorder-${index}`}
/>
</EuiFlexItem>
<EuiFlexItem
grow={true}
style={{ minWidth: 0 }}
className="lnsIndexPatternDimensionEditor__droppableItem"
>
<FieldSelect
fieldIsInvalid={shouldShowError}
currentIndexPattern={indexPattern}
existingFields={existingFields[indexPattern.title]}
operationByField={filteredOperationByField}
selectedOperationType={column.operationType}
selectedField={value}
autoFocus={isNew}
onChoose={(choice) => {
onFieldSelectChange(choice, index);
}}
isInvalid={shouldShowError}
data-test-subj={
localValues.length !== 1
? `indexPattern-dimension-field-${index}`
: undefined
}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<TooltipWrapper
tooltipContent={i18n.translate(
'xpack.lens.indexPattern.terms.deleteButtonDisabled',
{
defaultMessage:
'This function requires a minimum of one field defined',
}
)}
condition={disableActions}
>
<EuiButtonIcon
iconType="trash"
color="danger"
aria-label={i18n.translate(
'xpack.lens.indexPattern.terms.deleteButtonAriaLabel',
{
defaultMessage: 'Delete',
}
)}
title={i18n.translate(
'xpack.lens.indexPattern.terms.deleteButtonLabel',
{
defaultMessage: 'Delete',
}
)}
onClick={() => {
handleInputChange(localValues.filter((_, i) => i !== index));
}}
data-test-subj={`indexPattern-terms-removeField-${index}`}
isDisabled={disableActions && !isNew}
/>
</TooltipWrapper>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
)}
</EuiDraggable>
);
})}
</DragDropBuckets>
</div>
const shouldShowError = Boolean(
value &&
((indexPattern.getFieldByName(value)?.scripted && localValuesFilled.length > 1) ||
invalidFields?.includes(value))
);
const itemId = (value ?? 'newField') + id;
return (
<DraggableBucketContainer
id={itemId}
key={itemId}
idx={index}
onRemoveClick={() => {
handleInputChange(arrayRef.filter((_, i) => i !== index));
}}
removeTitle={i18n.translate('xpack.lens.indexPattern.terms.deleteButtonLabel', {
defaultMessage: 'Delete',
})}
isNotRemovable={disableActions && !isNew}
isNotDraggable={arrayRef.length < 2}
data-test-subj={`indexPattern-terms`}
Container={FieldsBucketContainer}
isInsidePanel={true}
>
<FieldSelect
fieldIsInvalid={shouldShowError}
currentIndexPattern={indexPattern}
existingFields={existingFields[indexPattern.title]}
operationByField={filteredOperationByField}
selectedOperationType={column.operationType}
selectedField={value}
autoFocus={isNew}
onChoose={(choice) => {
onFieldSelectChange(choice, index);
}}
isInvalid={shouldShowError}
data-test-subj={
localValues.length !== 1 ? `indexPattern-dimension-field-${index}` : undefined
}
/>
</DraggableBucketContainer>
);
})}
</DragDropBuckets>
<NewBucketButton
onClick={() => {
handleInputChange([...localValues, { id: generateId(), value: undefined, isNew: true }]);

View file

@ -63,14 +63,13 @@ describe('buckets shared components', () => {
it('should render invalid component', () => {
const instance = mount(<DraggableBucketContainer {...defaultProps} isInvalid />);
const iconProps = instance.find(EuiIcon).first().props();
expect(iconProps.color).toEqual('danger');
expect(iconProps.color).toEqual('#BD271E');
expect(iconProps.type).toEqual('alert');
expect(iconProps.title).toEqual('invalid');
});
it('should call onRemoveClick when remove icon is clicked', () => {
const instance = mount(<DraggableBucketContainer {...defaultProps} />);
const removeIcon = instance
.find('[data-test-subj="lns-customBucketContainer-remove"]')
.find('[data-test-subj="lns-customBucketContainer-remove-0"]')
.first();
removeIcon.simulate('click');
expect(defaultProps.onRemoveClick).toHaveBeenCalled();

View file

@ -5,162 +5,110 @@
* 2.0.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import React, { useCallback, useState } from 'react';
import type { Assign } from '@kbn/utility-types';
import {
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiButtonIcon,
EuiIcon,
EuiDragDropContext,
euiDragDropReorder,
EuiDraggable,
EuiDroppable,
EuiButtonEmpty,
EuiPanelProps,
EuiDragDropContext,
DragDropContextProps,
euiDragDropReorder,
useEuiTheme,
} from '@elastic/eui';
export const NewBucketButton = ({
label,
onClick,
['data-test-subj']: dataTestSubj,
isDisabled,
className,
}: {
label: string;
onClick: () => void;
'data-test-subj'?: string;
isDisabled?: boolean;
className?: string;
}) => (
<EuiButtonEmpty
data-test-subj={dataTestSubj ?? 'lns-newBucket-add'}
size="xs"
iconType="plusInCircle"
onClick={onClick}
isDisabled={isDisabled}
flush="left"
className={className}
>
{label}
</EuiButtonEmpty>
);
interface BucketContainerProps {
isInvalid?: boolean;
invalidMessage: string;
onRemoveClick: () => void;
removeTitle: string;
isNotRemovable?: boolean;
children: React.ReactNode;
dataTestSubj?: string;
}
const BucketContainer = ({
isInvalid,
invalidMessage,
onRemoveClick,
removeTitle,
children,
dataTestSubj,
isNotRemovable,
}: BucketContainerProps) => {
return (
<EuiPanel paddingSize="none" data-test-subj={dataTestSubj} hasShadow={false} hasBorder>
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
<EuiFlexItem grow={false}>{/* Empty for spacing */}</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiIcon
size="s"
color={isInvalid ? 'danger' : 'subdued'}
type={isInvalid ? 'alert' : 'grab'}
title={
isInvalid
? invalidMessage
: i18n.translate('xpack.lens.customBucketContainer.dragToReorder', {
defaultMessage: 'Drag to reorder',
})
}
/>
</EuiFlexItem>
<EuiFlexItem grow={true}>{children}</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
iconSize="s"
iconType="cross"
color="danger"
data-test-subj="lns-customBucketContainer-remove"
onClick={onRemoveClick}
aria-label={removeTitle}
title={removeTitle}
disabled={isNotRemovable}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
};
import { DefaultBucketContainer } from './default_bucket_container';
import type { BucketContainerProps } from './types';
export const DraggableBucketContainer = ({
id,
idx,
children,
isInsidePanel,
Container = DefaultBucketContainer,
...bucketContainerProps
}: {
id: string;
idx: number;
children: React.ReactNode;
} & BucketContainerProps) => {
}: Assign<
Omit<BucketContainerProps, 'draggableProvided'>,
{
id: string;
children: React.ReactNode;
isInsidePanel?: boolean;
Container?: React.FunctionComponent<BucketContainerProps>;
}
>) => {
const { euiTheme } = useEuiTheme();
return (
<EuiDraggable
style={{ marginBottom: 4 }}
spacing="none"
index={idx}
draggableId={id}
customDragHandle={true}
index={bucketContainerProps.idx}
isDragDisabled={bucketContainerProps.isNotDraggable}
style={!isInsidePanel ? { marginBottom: euiTheme.size.xs } : {}}
spacing="none"
hasInteractiveChildren
disableInteractiveElementBlocking
>
{(provided) => <BucketContainer {...bucketContainerProps}>{children}</BucketContainer>}
{(provided, state) => (
<Container
draggableProvided={provided}
isDragging={state?.isDragging ?? false}
{...bucketContainerProps}
>
{children}
</Container>
)}
</EuiDraggable>
);
};
interface DraggableLocation {
droppableId: string;
index: number;
}
export const DragDropBuckets = ({
export function DragDropBuckets<T = unknown>({
items,
onDragStart,
onDragEnd,
droppableId,
children,
className,
bgColor,
}: {
items: any; // eslint-disable-line @typescript-eslint/no-explicit-any
onDragStart: () => void;
onDragEnd: (items: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any
items: T[];
droppableId: string;
children: React.ReactElement[];
className?: string;
}) => {
const handleDragEnd = ({
source,
destination,
}: {
source?: DraggableLocation;
destination?: DraggableLocation;
}) => {
if (source && destination) {
const newItems = euiDragDropReorder(items, source.index, destination.index);
onDragEnd(newItems);
}
};
onDragStart?: () => void;
onDragEnd?: (items: T[]) => void;
bgColor?: EuiPanelProps['color'];
}) {
const [isDragging, setIsDragging] = useState(false);
const handleDragEnd: DragDropContextProps['onDragEnd'] = useCallback(
({ source, destination }) => {
setIsDragging(false);
if (source && destination) {
onDragEnd?.(euiDragDropReorder(items, source.index, destination.index));
}
},
[items, onDragEnd]
);
const handleDragStart: DragDropContextProps['onDragStart'] = useCallback(() => {
setIsDragging(true);
onDragStart?.();
}, [onDragStart]);
return (
<EuiDragDropContext onDragEnd={handleDragEnd} onDragStart={onDragStart}>
<EuiDroppable droppableId={droppableId} spacing="none" className={className}>
{children}
</EuiDroppable>
<EuiDragDropContext onDragEnd={handleDragEnd} onDragStart={handleDragStart}>
<EuiPanel
paddingSize="none"
color={isDragging ? 'success' : bgColor}
hasShadow={false}
hasBorder={false}
>
<EuiDroppable
droppableId={droppableId}
spacing={bgColor ? 'm' : 'none'}
style={{ backgroundColor: 'transparent' }}
>
{children}
</EuiDroppable>
</EuiPanel>
</EuiDragDropContext>
);
};
}

View file

@ -0,0 +1,93 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiPanel,
useEuiTheme,
} from '@elastic/eui';
import type { BucketContainerProps } from './types';
import { TooltipWrapper } from '../tooltip_wrapper';
export const DefaultBucketContainer = ({
idx,
isInvalid,
invalidMessage,
onRemoveClick,
removeTitle,
children,
draggableProvided,
isNotRemovable,
isNotDraggable,
'data-test-subj': dataTestSubj = 'lns-customBucketContainer',
}: BucketContainerProps) => {
const { euiTheme } = useEuiTheme();
return (
<EuiPanel
paddingSize="none"
hasShadow={false}
hasBorder={true}
data-test-subj={dataTestSubj}
style={{ padding: '0 ' + euiTheme.size.xs }}
>
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
<EuiFlexItem grow={false} {...(draggableProvided?.dragHandleProps ?? {})}>
<TooltipWrapper
tooltipContent={i18n.translate('xpack.lens.fieldsBucketContainer.dragHandleDisabled', {
defaultMessage: 'Reordering requires more than one item.',
})}
condition={isNotDraggable ?? true}
>
<EuiIcon
size="s"
color={
euiTheme.colors[isInvalid ? 'danger' : isNotDraggable ? 'disabled' : 'subduedText']
}
type={isInvalid ? 'alert' : 'grab'}
aria-label={
isInvalid
? invalidMessage
: i18n.translate('xpack.lens.customBucketContainer.dragToReorder', {
defaultMessage: 'Drag to reorder',
})
}
data-test-subj={`${dataTestSubj}-dragToReorder-${idx}`}
/>
</TooltipWrapper>
</EuiFlexItem>
<EuiFlexItem grow={true}>{children}</EuiFlexItem>
<EuiFlexItem grow={false}>
<TooltipWrapper
tooltipContent={i18n.translate(
'xpack.lens.fieldsBucketContainer.deleteButtonDisabled',
{
defaultMessage: 'A minimum of one item is required.',
}
)}
condition={isNotRemovable ?? false}
>
<EuiButtonIcon
iconSize="s"
iconType="trash"
color="danger"
onClick={onRemoveClick}
aria-label={removeTitle}
disabled={isNotRemovable}
data-test-subj={`${dataTestSubj}-remove-${idx}`}
/>
</TooltipWrapper>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
};

View file

@ -0,0 +1,86 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiPanel,
useEuiTheme,
} from '@elastic/eui';
import { TooltipWrapper } from '..';
import type { BucketContainerProps } from './types';
export const FieldsBucketContainer = ({
idx,
onRemoveClick,
removeTitle,
children,
draggableProvided,
isNotRemovable,
isNotDraggable,
isDragging,
'data-test-subj': dataTestSubj = 'lns-fieldsBucketContainer',
}: BucketContainerProps) => {
const { euiTheme } = useEuiTheme();
return (
<EuiPanel
paddingSize="xs"
hasShadow={isDragging}
color={isDragging ? 'plain' : 'transparent'}
data-test-subj={dataTestSubj}
>
<EuiFlexGroup direction={'row'} gutterSize="s" alignItems="center" responsive={false}>
<EuiFlexItem grow={false} {...(draggableProvided?.dragHandleProps ?? {})}>
<TooltipWrapper
tooltipContent={i18n.translate('xpack.lens.fieldsBucketContainer.dragHandleDisabled', {
defaultMessage: 'Reordering requires more than one item.',
})}
condition={isNotDraggable ?? true}
>
<EuiIcon
size="s"
color={euiTheme.colors[isNotDraggable ? 'disabled' : 'text']}
type="grab"
aria-label={i18n.translate('xpack.lens.fieldsBucketContainer.dragToReorder', {
defaultMessage: 'Drag to reorder',
})}
data-test-subj={`${dataTestSubj}-dragToReorder-${idx}`}
/>
</TooltipWrapper>
</EuiFlexItem>
<EuiFlexItem grow={true} style={{ minWidth: 0 }}>
{children}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<TooltipWrapper
tooltipContent={i18n.translate(
'xpack.lens.fieldsBucketContainer.deleteButtonDisabled',
{
defaultMessage: 'A minimum of one item is required.',
}
)}
condition={isNotRemovable ?? false}
>
<EuiButtonIcon
iconType="trash"
color="danger"
aria-label={removeTitle}
onClick={onRemoveClick}
data-test-subj={`${dataTestSubj}-removeField-${idx}`}
isDisabled={isNotRemovable}
/>
</TooltipWrapper>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
};

View file

@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { NewBucketButton } from './new_bucket_button';
export { FieldsBucketContainer } from './fields_bucket_container';
export * from './buckets';

View file

@ -0,0 +1,37 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiButtonEmpty } from '@elastic/eui';
interface NewBucketButtonProps {
label: string;
onClick: () => void;
isDisabled?: boolean;
className?: string;
'data-test-subj'?: string;
}
export const NewBucketButton = ({
label,
onClick,
isDisabled,
className,
'data-test-subj': dataTestSubj = 'lns-newBucket-add',
}: NewBucketButtonProps) => (
<EuiButtonEmpty
data-test-subj={dataTestSubj}
size="xs"
iconType="plusInCircle"
onClick={onClick}
isDisabled={isDisabled}
flush="left"
className={className}
>
{label}
</EuiButtonEmpty>
);

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import type { DraggableProvided } from 'react-beautiful-dnd';
export interface BucketContainerProps {
children: React.ReactNode;
removeTitle: string;
idx: number;
onRemoveClick: () => void;
isDragging?: boolean;
draggableProvided?: DraggableProvided;
isInvalid?: boolean;
invalidMessage?: string;
isNotRemovable?: boolean;
isNotDraggable?: boolean;
'data-test-subj'?: string;
}

View file

@ -17,7 +17,8 @@ export {
NewBucketButton,
DraggableBucketContainer,
DragDropBuckets,
} from './drag_drop_bucket/buckets';
FieldsBucketContainer,
} from './drag_drop_bucket';
export { RangeInputField } from './range_input_field';
export {
BucketAxisBoundsControl,

View file

@ -14,11 +14,6 @@
margin-top: $euiSizeXS;
}
.lnsConfigPanelAnnotations__droppable {
padding: $euiSizeXS;
border-radius: $euiBorderRadiusSmall;
}
.lnsConfigPanelAnnotations__fieldPicker {
cursor: pointer;
}
}

View file

@ -5,19 +5,9 @@
* 2.0.
*/
import {
htmlIdGenerator,
EuiButtonIcon,
EuiDraggable,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiPanel,
useEuiTheme,
EuiText,
} from '@elastic/eui';
import { htmlIdGenerator, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useMemo } from 'react';
import { QueryPointEventAnnotationConfig } from '@kbn/event-annotation-plugin/common';
import type { ExistingFieldsMap, IndexPattern } from '../../../../types';
import {
@ -28,8 +18,12 @@ import {
useDebouncedValue,
NewBucketButton,
DragDropBuckets,
DraggableBucketContainer,
FieldsBucketContainer,
} from '../../../../shared_components';
export const MAX_TOOLTIP_FIELDS_SIZE = 2;
const generateId = htmlIdGenerator();
const supportedTypes = new Set(['string', 'boolean', 'number', 'ip', 'date']);
@ -60,8 +54,6 @@ export function TooltipSection({
existingFields,
invalidFields,
}: FieldInputsProps) {
const { euiTheme } = useEuiTheme();
const [isDragging, setIsDragging] = useState(false);
const onChangeWrapped = useCallback(
(values: WrappedValue[]) => {
setConfig({
@ -108,6 +100,7 @@ export function TooltipSection({
label={i18n.translate('xpack.lens.xyChart.annotation.tooltip.addField', {
defaultMessage: 'Add field',
})}
isDisabled={localValues.length > MAX_TOOLTIP_FIELDS_SIZE}
/>
);
@ -132,7 +125,7 @@ export function TooltipSection({
);
}
const currentExistingField = existingFields[indexPattern.title];
const disableActions = localValues.length === 2 && localValues.some(({ isNew }) => isNew);
const options = indexPattern.fields
.filter(
({ displayName, type }) =>
@ -154,107 +147,62 @@ export function TooltipSection({
)
.sort((a, b) => a.label.localeCompare(b.label));
const isDragDisabled = localValues.length < 2;
return (
<>
<div
style={{
backgroundColor: isDragging ? 'transparent' : euiTheme.colors.lightestShade,
borderRadius: euiTheme.border.radius.small,
<DragDropBuckets
onDragEnd={(updatedValues: WrappedValue[]) => {
handleInputChange(updatedValues);
}}
droppableId="ANNOTATION_TOOLTIP_DROPPABLE_AREA"
items={localValues}
bgColor="subdued"
>
<DragDropBuckets
onDragEnd={(updatedValues: WrappedValue[]) => {
handleInputChange(updatedValues);
setIsDragging(false);
}}
onDragStart={() => {
setIsDragging(true);
}}
droppableId="ANNOTATION_TOOLTIP_DROPPABLE_AREA"
items={localValues}
className="lnsConfigPanelAnnotations__droppable"
>
{localValues.map(({ id, value, isNew }, index) => {
const fieldIsValid = value ? Boolean(indexPattern.getFieldByName(value)) : true;
return (
<EuiDraggable
spacing="none"
index={index}
draggableId={value || 'newField'}
key={id}
disableInteractiveElementBlocking
isDragDisabled={isDragDisabled}
>
{(provided) => (
<EuiPanel paddingSize="xs" hasShadow={false} color="transparent">
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
<EuiFlexItem grow={false}>{/* Empty for spacing */}</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiIcon
size="s"
color={isDragDisabled ? euiTheme.colors.disabled : 'subdued'}
type="grab"
title={i18n.translate(
'xpack.lens.xyChart.annotation..tooltip.dragToReorder',
{
defaultMessage: 'Drag to reorder',
}
)}
data-test-subj={`lnsXY-annotation-tooltip-dragToReorder-${index}`}
/>
</EuiFlexItem>
<EuiFlexItem grow={true} style={{ minWidth: 0 }}>
<FieldPicker
selectedOptions={
value
? [
{
label: value,
value: { type: 'field', field: value },
},
]
: []
}
options={options}
onChoose={function (choice: FieldOptionValue | undefined): void {
onFieldSelectChange(choice, index);
}}
fieldIsInvalid={!fieldIsValid}
className="lnsConfigPanelAnnotations__fieldPicker"
data-test-subj={`lnsXY-annotation-tooltip-field-picker--${index}`}
autoFocus={isNew && value == null}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
iconType="trash"
color="danger"
aria-label={i18n.translate(
'xpack.lens.indexPattern.terms.deleteButtonAriaLabel',
{
defaultMessage: 'Delete',
}
)}
title={i18n.translate('xpack.lens.indexPattern.terms.deleteButtonLabel', {
defaultMessage: 'Delete',
})}
onClick={() => {
handleInputChange(localValues.filter((_, i) => i !== index));
}}
data-test-subj={`lnsXY-annotation-tooltip-removeField-${index}`}
isDisabled={disableActions && !isNew}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
)}
</EuiDraggable>
);
})}
</DragDropBuckets>
</div>
{localValues.map(({ id, value, isNew }, index, arrayRef) => {
const fieldIsValid = value ? Boolean(indexPattern.getFieldByName(value)) : true;
return (
<DraggableBucketContainer
id={(value ?? 'newField') + id}
key={(value ?? 'newField') + id}
idx={index}
onRemoveClick={() => {
handleInputChange(arrayRef.filter((_, i) => i !== index));
}}
removeTitle={i18n.translate(
'xpack.lens.xyChart.annotation.tooltip.deleteButtonLabel',
{
defaultMessage: 'Delete',
}
)}
isNotDraggable={arrayRef.length < 2}
Container={FieldsBucketContainer}
isInsidePanel={true}
data-test-subj={`lnsXY-annotation-tooltip-${index}`}
>
<FieldPicker
selectedOptions={
value
? [
{
label: value,
value: { type: 'field', field: value },
},
]
: []
}
options={options}
onChoose={(choice) => {
onFieldSelectChange(choice, index);
}}
fieldIsInvalid={!fieldIsValid}
className="lnsConfigPanelAnnotations__fieldPicker"
data-test-subj={`lnsXY-annotation-tooltip-field-picker--${index}`}
autoFocus={isNew && value == null}
/>
</DraggableBucketContainer>
);
})}
</DragDropBuckets>
{newBucketButton}
</>
);

View file

@ -17838,10 +17838,7 @@
"xpack.lens.indexPattern.terms.addaFilter": "Ajouter un champ",
"xpack.lens.indexPattern.terms.addRegex": "Utiliser une expression régulière",
"xpack.lens.indexPattern.terms.advancedSettings": "Avancé",
"xpack.lens.indexPattern.terms.deleteButtonAriaLabel": "Supprimer",
"xpack.lens.indexPattern.terms.deleteButtonDisabled": "Cette fonction nécessite au minimum un champ défini.",
"xpack.lens.indexPattern.terms.deleteButtonLabel": "Supprimer",
"xpack.lens.indexPattern.terms.dragToReorder": "Faire glisser pour réorganiser",
"xpack.lens.indexPattern.terms.exclude": "Exclure les valeurs",
"xpack.lens.indexPattern.terms.include": "Inclure les valeurs",
"xpack.lens.indexPattern.terms.includeExcludePatternPlaceholder": "Entrer une expression régulière pour filtrer les valeurs",

View file

@ -17821,10 +17821,7 @@
"xpack.lens.indexPattern.terms.addaFilter": "フィールドの追加",
"xpack.lens.indexPattern.terms.addRegex": "正規表現を使用",
"xpack.lens.indexPattern.terms.advancedSettings": "高度な設定",
"xpack.lens.indexPattern.terms.deleteButtonAriaLabel": "削除",
"xpack.lens.indexPattern.terms.deleteButtonDisabled": "この関数には定義された1つのフィールドの最小値が必須です",
"xpack.lens.indexPattern.terms.deleteButtonLabel": "削除",
"xpack.lens.indexPattern.terms.dragToReorder": "ドラッグして並べ替え",
"xpack.lens.indexPattern.terms.exclude": "値を除外",
"xpack.lens.indexPattern.terms.include": "値を含める",
"xpack.lens.indexPattern.terms.includeExcludePatternPlaceholder": "値をフィルターするには正規表現を入力します",

View file

@ -17846,10 +17846,7 @@
"xpack.lens.indexPattern.terms.addaFilter": "添加字段",
"xpack.lens.indexPattern.terms.addRegex": "使用正则表达式",
"xpack.lens.indexPattern.terms.advancedSettings": "高级",
"xpack.lens.indexPattern.terms.deleteButtonAriaLabel": "删除",
"xpack.lens.indexPattern.terms.deleteButtonDisabled": "此函数需要至少定义一个字段",
"xpack.lens.indexPattern.terms.deleteButtonLabel": "删除",
"xpack.lens.indexPattern.terms.dragToReorder": "拖动以重新排序",
"xpack.lens.indexPattern.terms.exclude": "排除值",
"xpack.lens.indexPattern.terms.include": "包括值",
"xpack.lens.indexPattern.terms.includeExcludePatternPlaceholder": "输入正则表达式以筛选值",