mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Discover] Drag & drop for adding columns to the table (#153538)
Closes https://github.com/elastic/kibana/issues/151703 ## Summary This PR: - changes design of field list item in Lens and Discover - changes design of dimension triggers in Lens panels - changes design "Add layers" button - adds "+" action to field items in Lens - unifies the logic of rendering Lens field items for text-based and form-based views - adds Drag&Drop functionality to Discover to add columns to the table by dragging a field from the sidebar - adds functional tests for drag&drop in Discover - shows fields popover in text-based mode too so users can copy field names [Figma](https://www.figma.com/file/SvpfCqaZPb2iAYnPtd0Gnr/KUI-Library?node-id=674%3A198901&t=OnQH2EQ4fdBjsRLp-0) <details> <summary>Gifs</summary>   </details> ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Andrea Del Rio <delrio.andre@gmail.com> Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
This commit is contained in:
parent
0225747610
commit
cac928b956
54 changed files with 1628 additions and 748 deletions
|
@ -16,6 +16,8 @@ export {
|
|||
RootDragDropProvider,
|
||||
ChildDragDropProvider,
|
||||
ReorderProvider,
|
||||
DropOverlayWrapper,
|
||||
type DropOverlayWrapperProps,
|
||||
} from './src';
|
||||
|
||||
export { DropTargetSwapDuplicateCombine } from './src/drop_targets';
|
||||
|
|
|
@ -7,3 +7,5 @@
|
|||
*/
|
||||
|
||||
export const DEFAULT_DATA_TEST_SUBJ = 'domDragDrop';
|
||||
export const REORDER_ITEM_HEIGHT = 32;
|
||||
export const REORDER_ITEM_MARGIN = 8;
|
||||
|
|
|
@ -468,7 +468,7 @@ describe('DragDrop', () => {
|
|||
ghost: {
|
||||
children: <button>Hi!</button>,
|
||||
style: {
|
||||
height: 0,
|
||||
minHeight: 0,
|
||||
width: 0,
|
||||
},
|
||||
},
|
||||
|
@ -1100,12 +1100,12 @@ describe('DragDrop', () => {
|
|||
expect(
|
||||
component.find('[data-test-subj="testDragDrop-translatableDrop"]').at(0).prop('style')
|
||||
).toEqual({
|
||||
transform: 'translateY(-8px)',
|
||||
transform: 'translateY(-4px)',
|
||||
});
|
||||
expect(
|
||||
component.find('[data-test-subj="testDragDrop-translatableDrop"]').at(1).prop('style')
|
||||
).toEqual({
|
||||
transform: 'translateY(-8px)',
|
||||
transform: 'translateY(-4px)',
|
||||
});
|
||||
|
||||
component
|
||||
|
@ -1258,12 +1258,12 @@ describe('DragDrop', () => {
|
|||
expect(
|
||||
component.find('[data-test-subj="testDragDrop-reorderableDrag"]').at(0).prop('style')
|
||||
).toEqual({
|
||||
transform: 'translateY(+8px)',
|
||||
transform: 'translateY(+4px)',
|
||||
});
|
||||
expect(
|
||||
component.find('[data-test-subj="testDragDrop-translatableDrop"]').at(0).prop('style')
|
||||
).toEqual({
|
||||
transform: 'translateY(-40px)',
|
||||
transform: 'translateY(-32px)',
|
||||
});
|
||||
expect(
|
||||
component.find('[data-test-subj="testDragDrop-translatableDrop"]').at(1).prop('style')
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
Ghost,
|
||||
} from './providers';
|
||||
import { DropType } from './types';
|
||||
import { REORDER_ITEM_MARGIN } from './constants';
|
||||
import './sass/drag_drop.scss';
|
||||
|
||||
/**
|
||||
|
@ -71,11 +72,15 @@ interface BaseProps {
|
|||
* The React element which will be passed the draggable handlers
|
||||
*/
|
||||
children: ReactElement;
|
||||
|
||||
/**
|
||||
* Disable any drag & drop behaviour
|
||||
*/
|
||||
isDisabled?: boolean;
|
||||
/**
|
||||
* Indicates whether or not this component is draggable.
|
||||
*/
|
||||
draggable?: boolean;
|
||||
|
||||
/**
|
||||
* Additional class names to apply when another element is over the drop target
|
||||
*/
|
||||
|
@ -152,7 +157,7 @@ interface DropsInnerProps extends BaseProps {
|
|||
isNotDroppable: boolean;
|
||||
}
|
||||
|
||||
const lnsLayerPanelDimensionMargin = 8;
|
||||
const REORDER_OFFSET = REORDER_ITEM_MARGIN / 2;
|
||||
|
||||
/**
|
||||
* DragDrop component
|
||||
|
@ -174,6 +179,10 @@ export const DragDrop = (props: BaseProps) => {
|
|||
onTrackUICounterEvent,
|
||||
} = useContext(DragContext);
|
||||
|
||||
if (props.isDisabled) {
|
||||
return props.children;
|
||||
}
|
||||
|
||||
const { value, draggable, dropTypes, reorderableGroup } = props;
|
||||
const isDragging = !!(draggable && value.id === dragging?.id);
|
||||
|
||||
|
@ -358,7 +367,7 @@ const DragInner = memo(function DragInner({
|
|||
ghost: keyboardModeOn
|
||||
? {
|
||||
children,
|
||||
style: { width: currentTarget.offsetWidth, height: currentTarget.offsetHeight },
|
||||
style: { width: currentTarget.offsetWidth, minHeight: currentTarget?.offsetHeight },
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
@ -417,12 +426,14 @@ const DragInner = memo(function DragInner({
|
|||
keyboardMode &&
|
||||
activeDropTarget &&
|
||||
activeDropTarget.dropType !== 'reorder';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(className, {
|
||||
'domDragDrop-isHidden':
|
||||
(activeDraggingProps && dragType === 'move' && !keyboardMode) ||
|
||||
shouldShowGhostImageInstead,
|
||||
'domDragDrop--isDragStarted': activeDraggingProps,
|
||||
})}
|
||||
data-test-subj={`${dataTestSubjPrefix}_draggable-${value.humanData.label}`}
|
||||
>
|
||||
|
@ -772,7 +783,7 @@ const ReorderableDrag = memo(function ReorderableDrag(
|
|||
| KeyboardEvent<HTMLButtonElement>['currentTarget']
|
||||
) => {
|
||||
if (currentTarget) {
|
||||
const height = currentTarget.offsetHeight + lnsLayerPanelDimensionMargin;
|
||||
const height = currentTarget.offsetHeight + REORDER_OFFSET;
|
||||
setReorderState((s: ReorderState) => ({
|
||||
...s,
|
||||
draggingHeight: height,
|
||||
|
@ -875,7 +886,7 @@ const ReorderableDrag = memo(function ReorderableDrag(
|
|||
areItemsReordered
|
||||
? {
|
||||
transform: `translateY(${direction === '+' ? '-' : '+'}${reorderedItems.reduce(
|
||||
(acc, cur) => acc + Number(cur.height || 0) + lnsLayerPanelDimensionMargin,
|
||||
(acc, cur) => acc + Number(cur.height || 0) + REORDER_OFFSET,
|
||||
0
|
||||
)}px)`,
|
||||
}
|
||||
|
|
52
packages/kbn-dom-drag-drop/src/drop_overlay_wrapper.tsx
Normal file
52
packages/kbn-dom-drag-drop/src/drop_overlay_wrapper.tsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
/**
|
||||
* DropOverlayWrapper Props
|
||||
*/
|
||||
export interface DropOverlayWrapperProps {
|
||||
isVisible: boolean;
|
||||
className?: string;
|
||||
overlayProps?: object;
|
||||
}
|
||||
|
||||
/**
|
||||
* This prevents the in-place droppable styles (under children) and allows to rather show an overlay with droppable styles (on top of children)
|
||||
* @param isVisible
|
||||
* @param children
|
||||
* @param overlayProps
|
||||
* @param className
|
||||
* @param otherProps
|
||||
* @constructor
|
||||
*/
|
||||
export const DropOverlayWrapper: React.FC<DropOverlayWrapperProps> = ({
|
||||
isVisible,
|
||||
children,
|
||||
overlayProps,
|
||||
className,
|
||||
...otherProps
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={classnames('domDragDrop__dropOverlayWrapper', className)}
|
||||
{...(otherProps || {})}
|
||||
>
|
||||
{children}
|
||||
{isVisible && (
|
||||
<div
|
||||
className="domDragDrop__dropOverlay"
|
||||
data-test-subj="domDragDrop__dropOverlay"
|
||||
{...(overlayProps || {})}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -9,3 +9,4 @@
|
|||
export * from './types';
|
||||
export * from './providers';
|
||||
export * from './drag_drop';
|
||||
export { DropOverlayWrapper, type DropOverlayWrapperProps } from './drop_overlay_wrapper';
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { DEFAULT_DATA_TEST_SUBJ } from '../constants';
|
||||
import { DEFAULT_DATA_TEST_SUBJ, REORDER_ITEM_HEIGHT } from '../constants';
|
||||
|
||||
/**
|
||||
* Reorder state
|
||||
|
@ -54,7 +54,7 @@ export const ReorderContext = React.createContext<ReorderContextState>({
|
|||
reorderState: {
|
||||
reorderedItems: [],
|
||||
direction: '-',
|
||||
draggingHeight: 40,
|
||||
draggingHeight: REORDER_ITEM_HEIGHT,
|
||||
isReorderOn: false,
|
||||
groupId: '',
|
||||
},
|
||||
|
@ -66,6 +66,7 @@ export const ReorderContext = React.createContext<ReorderContextState>({
|
|||
* @param id
|
||||
* @param children
|
||||
* @param className
|
||||
* @param draggingHeight
|
||||
* @param dataTestSubj
|
||||
* @constructor
|
||||
*/
|
||||
|
@ -73,17 +74,19 @@ export function ReorderProvider({
|
|||
id,
|
||||
children,
|
||||
className,
|
||||
draggingHeight = REORDER_ITEM_HEIGHT,
|
||||
dataTestSubj = DEFAULT_DATA_TEST_SUBJ,
|
||||
}: {
|
||||
id: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
draggingHeight?: number;
|
||||
dataTestSubj?: string;
|
||||
}) {
|
||||
const [state, setState] = useState<ReorderContextState['reorderState']>({
|
||||
reorderedItems: [],
|
||||
direction: '-',
|
||||
draggingHeight: 40,
|
||||
draggingHeight,
|
||||
isReorderOn: false,
|
||||
groupId: id,
|
||||
});
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
@import './drag_drop_mixins';
|
||||
|
||||
.domDragDrop {
|
||||
user-select: none;
|
||||
transition: $euiAnimSpeedFast ease-in-out;
|
||||
transition-property: background-color, border-color, opacity;
|
||||
z-index: $domDragDropZLevel1;
|
||||
|
@ -12,11 +11,11 @@
|
|||
border: $euiBorderWidthThin dashed $euiBorderColor;
|
||||
position: absolute !important; // sass-lint:disable-line no-important
|
||||
margin: 0 !important; // sass-lint:disable-line no-important
|
||||
bottom: 100%;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
opacity: .9;
|
||||
transform: translate(-12px, 8px);
|
||||
transform: translate($euiSizeXS, $euiSizeXS);
|
||||
z-index: $domDragDropZLevel3;
|
||||
pointer-events: none;
|
||||
outline: $euiFocusRingSize solid currentColor; // Safari & Firefox
|
||||
|
@ -29,7 +28,8 @@
|
|||
@include mixinDomDragDropHover;
|
||||
|
||||
// Include a possible nested button like when using FieldButton
|
||||
> .kbnFieldButton__button {
|
||||
& .kbnFieldButton__button,
|
||||
& .euiLink {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
|
@ -39,14 +39,17 @@
|
|||
}
|
||||
|
||||
// Drop area
|
||||
.domDragDrop-isDroppable {
|
||||
.domDragDrop-isDroppable:not(.domDragDrop__dropOverlayWrapper) {
|
||||
@include mixinDomDroppable;
|
||||
}
|
||||
|
||||
// Drop area when there's an item being dragged
|
||||
.domDragDrop-isDropTarget {
|
||||
@include mixinDomDroppable;
|
||||
@include mixinDomDroppableActive;
|
||||
&:not(.domDragDrop__dropOverlayWrapper) {
|
||||
@include mixinDomDroppable;
|
||||
@include mixinDomDroppableActive;
|
||||
}
|
||||
|
||||
> * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
@ -57,7 +60,7 @@
|
|||
}
|
||||
|
||||
// Drop area while hovering with item
|
||||
.domDragDrop-isActiveDropTarget {
|
||||
.domDragDrop-isActiveDropTarget:not(.domDragDrop__dropOverlayWrapper) {
|
||||
z-index: $domDragDropZLevel3;
|
||||
@include mixinDomDroppableActiveHover;
|
||||
}
|
||||
|
@ -72,9 +75,9 @@
|
|||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.domDragDrop-notCompatible {
|
||||
.domDragDrop-notCompatible:not(.domDragDrop__dropOverlayWrapper) {
|
||||
background-color: $euiColorHighlight !important;
|
||||
border: $euiBorderWidthThin dashed $euiBorderColor !important;
|
||||
border: $euiBorderWidthThin dashed $euiColorVis5 !important;
|
||||
&.domDragDrop-isActiveDropTarget {
|
||||
background-color: rgba(251, 208, 17, .25) !important;
|
||||
border-color: $euiColorVis5 !important;
|
||||
|
@ -91,12 +94,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
$lnsLayerPanelDimensionMargin: 8px;
|
||||
$reorderItemMargin: $euiSizeS;
|
||||
.domDragDrop__reorderableDrop {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
height: calc(100% + #{$lnsLayerPanelDimensionMargin});
|
||||
height: calc(100% + #{$reorderItemMargin / 2});
|
||||
}
|
||||
|
||||
.domDragDrop-translatableDrop {
|
||||
|
@ -146,6 +149,10 @@ $lnsLayerPanelDimensionMargin: 8px;
|
|||
}
|
||||
}
|
||||
|
||||
.domDragDrop--isDragStarted {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.domDragDrop__extraDrops {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
|
@ -193,7 +200,7 @@ $lnsLayerPanelDimensionMargin: 8px;
|
|||
|
||||
.domDragDrop__extraDrop {
|
||||
position: relative;
|
||||
height: $euiSizeXS * 10;
|
||||
height: $euiSizeXS * 8;
|
||||
min-width: $euiSize * 7;
|
||||
color: $euiColorSuccessText;
|
||||
padding: $euiSizeXS;
|
||||
|
@ -201,3 +208,28 @@ $lnsLayerPanelDimensionMargin: 8px;
|
|||
color: $euiColorWarningText;
|
||||
}
|
||||
}
|
||||
|
||||
.domDragDrop__dropOverlayWrapper {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.domDragDrop__dropOverlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: $domDragDropZLevel3;
|
||||
transition: $euiAnimSpeedFast ease-in-out;
|
||||
transition-property: background-color, border-color, opacity;
|
||||
|
||||
.domDragDrop-isDropTarget & {
|
||||
@include mixinDomDroppable($euiBorderWidthThick);
|
||||
@include mixinDomDroppableActive($euiBorderWidthThick);
|
||||
}
|
||||
|
||||
.domDragDrop-isActiveDropTarget & {
|
||||
@include mixinDomDroppableActiveHover($euiBorderWidthThick);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,31 +13,32 @@ $domDragDropZLevel3: 3;
|
|||
@mixin mixinDomDraggable {
|
||||
@include euiSlightShadow;
|
||||
background: $euiColorEmptyShade;
|
||||
border: $euiBorderWidthThin dashed transparent;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
// Static styles for a drop area
|
||||
@mixin mixinDomDroppable {
|
||||
border: $euiBorderWidthThin dashed $euiBorderColor !important;
|
||||
@mixin mixinDomDroppable($borderWidth: $euiBorderWidthThin) {
|
||||
border: $borderWidth dashed transparent;
|
||||
}
|
||||
|
||||
// Hovering state for drag item and drop area
|
||||
@mixin mixinDomDragDropHover {
|
||||
&:hover {
|
||||
border: $euiBorderWidthThin dashed $euiColorMediumShade !important;
|
||||
transform: translateX($euiSizeXS);
|
||||
transition: transform $euiAnimSpeedSlow ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
// Style for drop area when there's an item being dragged
|
||||
@mixin mixinDomDroppableActive {
|
||||
@mixin mixinDomDroppableActive($borderWidth: $euiBorderWidthThin) {
|
||||
background-color: transparentize($euiColorVis0, .9) !important;
|
||||
border: $borderWidth dashed $euiColorVis0 !important;
|
||||
}
|
||||
|
||||
// Style for drop area while hovering with item
|
||||
@mixin mixinDomDroppableActiveHover {
|
||||
@mixin mixinDomDroppableActiveHover($borderWidth: $euiBorderWidthThin) {
|
||||
background-color: transparentize($euiColorVis0, .75) !important;
|
||||
border: $euiBorderWidthThin dashed $euiColorVis0 !important;
|
||||
border: $borderWidth dashed $euiColorVis0 !important;
|
||||
}
|
||||
|
||||
// Style for drop area that is not allowed for current item
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
exports[`fieldAction is rendered 1`] = `
|
||||
<div
|
||||
className="kbnFieldButton"
|
||||
className="kbnFieldButton kbnFieldButton--s"
|
||||
>
|
||||
<button
|
||||
className="kbn-resetFocusState kbnFieldButton__button"
|
||||
|
@ -11,7 +11,11 @@ exports[`fieldAction is rendered 1`] = `
|
|||
<span
|
||||
className="kbnFieldButton__name"
|
||||
>
|
||||
name
|
||||
<span
|
||||
className="kbnFieldButton__nameInner"
|
||||
>
|
||||
name
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
|
@ -26,7 +30,7 @@ exports[`fieldAction is rendered 1`] = `
|
|||
|
||||
exports[`fieldIcon is rendered 1`] = `
|
||||
<div
|
||||
className="kbnFieldButton"
|
||||
className="kbnFieldButton kbnFieldButton--s"
|
||||
>
|
||||
<button
|
||||
className="kbn-resetFocusState kbnFieldButton__button"
|
||||
|
@ -42,7 +46,11 @@ exports[`fieldIcon is rendered 1`] = `
|
|||
<span
|
||||
className="kbnFieldButton__name"
|
||||
>
|
||||
name
|
||||
<span
|
||||
className="kbnFieldButton__nameInner"
|
||||
>
|
||||
name
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -50,7 +58,7 @@ exports[`fieldIcon is rendered 1`] = `
|
|||
|
||||
exports[`isActive defaults to false 1`] = `
|
||||
<div
|
||||
className="kbnFieldButton"
|
||||
className="kbnFieldButton kbnFieldButton--s"
|
||||
>
|
||||
<button
|
||||
className="kbn-resetFocusState kbnFieldButton__button"
|
||||
|
@ -59,7 +67,11 @@ exports[`isActive defaults to false 1`] = `
|
|||
<span
|
||||
className="kbnFieldButton__name"
|
||||
>
|
||||
name
|
||||
<span
|
||||
className="kbnFieldButton__nameInner"
|
||||
>
|
||||
name
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -67,7 +79,7 @@ exports[`isActive defaults to false 1`] = `
|
|||
|
||||
exports[`isActive renders true 1`] = `
|
||||
<div
|
||||
className="kbnFieldButton kbnFieldButton-isActive"
|
||||
className="kbnFieldButton kbnFieldButton--s kbnFieldButton-isActive"
|
||||
>
|
||||
<button
|
||||
className="kbn-resetFocusState kbnFieldButton__button"
|
||||
|
@ -76,41 +88,11 @@ exports[`isActive renders true 1`] = `
|
|||
<span
|
||||
className="kbnFieldButton__name"
|
||||
>
|
||||
name
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`isDraggable is rendered 1`] = `
|
||||
<div
|
||||
className="kbnFieldButton kbnFieldButton--isDraggable"
|
||||
>
|
||||
<button
|
||||
className="kbn-resetFocusState kbnFieldButton__button"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span
|
||||
className="kbnFieldButton__name"
|
||||
>
|
||||
name
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`sizes m is applied 1`] = `
|
||||
<div
|
||||
className="kbnFieldButton"
|
||||
>
|
||||
<button
|
||||
className="kbn-resetFocusState kbnFieldButton__button"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span
|
||||
className="kbnFieldButton__name"
|
||||
>
|
||||
name
|
||||
<span
|
||||
className="kbnFieldButton__nameInner"
|
||||
>
|
||||
name
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -118,7 +100,7 @@ exports[`sizes m is applied 1`] = `
|
|||
|
||||
exports[`sizes s is applied 1`] = `
|
||||
<div
|
||||
className="kbnFieldButton kbnFieldButton--small"
|
||||
className="kbnFieldButton kbnFieldButton--s"
|
||||
>
|
||||
<button
|
||||
className="kbn-resetFocusState kbnFieldButton__button"
|
||||
|
@ -127,7 +109,60 @@ exports[`sizes s is applied 1`] = `
|
|||
<span
|
||||
className="kbnFieldButton__name"
|
||||
>
|
||||
name
|
||||
<span
|
||||
className="kbnFieldButton__nameInner"
|
||||
>
|
||||
name
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`sizes xs is applied 1`] = `
|
||||
<div
|
||||
className="kbnFieldButton kbnFieldButton--xs"
|
||||
>
|
||||
<button
|
||||
className="kbn-resetFocusState kbnFieldButton__button"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span
|
||||
className="kbnFieldButton__name"
|
||||
>
|
||||
<span
|
||||
className="kbnFieldButton__nameInner"
|
||||
>
|
||||
name
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`with drag handle is rendered 1`] = `
|
||||
<div
|
||||
className="kbnFieldButton kbnFieldButton--s"
|
||||
>
|
||||
<div
|
||||
className="kbnFieldButton__dragHandle"
|
||||
>
|
||||
<span>
|
||||
drag
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
className="kbn-resetFocusState kbnFieldButton__button"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span
|
||||
className="kbnFieldButton__name"
|
||||
>
|
||||
<span
|
||||
className="kbnFieldButton__nameInner"
|
||||
>
|
||||
name
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -2,8 +2,9 @@
|
|||
@include euiFontSizeS;
|
||||
border-radius: $euiBorderRadius;
|
||||
margin-bottom: $euiSizeXS;
|
||||
padding: 0 $euiSizeS;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
transition: box-shadow $euiAnimSpeedFast $euiAnimSlightResistance,
|
||||
background-color $euiAnimSpeedFast $euiAnimSlightResistance; // sass-lint:disable-line indentation
|
||||
|
||||
|
@ -13,29 +14,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.kbnFieldButton--isDraggable {
|
||||
background: lightOrDarkTheme($euiColorEmptyShade, $euiColorLightestShade);
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:focus-within {
|
||||
@include euiBottomShadowMedium;
|
||||
border-radius: $euiBorderRadius;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.kbnFieldButton__button {
|
||||
&:hover,
|
||||
&:focus {
|
||||
cursor: grab;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.kbnFieldButton__button {
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
padding: $euiSizeS;
|
||||
padding: $euiSizeS 0;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
line-height: normal;
|
||||
|
@ -44,33 +26,55 @@
|
|||
.kbnFieldButton__fieldIcon {
|
||||
flex-shrink: 0;
|
||||
line-height: 0;
|
||||
margin-right: $euiSizeS;
|
||||
}
|
||||
|
||||
.kbnFieldButton__name {
|
||||
flex-grow: 1;
|
||||
word-break: break-word;
|
||||
padding: 0 $euiSizeS;
|
||||
}
|
||||
|
||||
.kbnFieldButton__infoIcon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: $euiSize;
|
||||
flex-shrink: 0;
|
||||
margin-left: $euiSizeXS;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.kbnFieldButton__fieldAction {
|
||||
margin-left: $euiSizeS;
|
||||
}
|
||||
|
||||
.kbnFieldButton__dragHandle {
|
||||
margin-right: $euiSizeS;
|
||||
}
|
||||
|
||||
.kbnFieldButton__fieldAction,
|
||||
.kbnFieldButton__dragHandle {
|
||||
line-height: $euiSizeXL;
|
||||
}
|
||||
|
||||
// Reduce text size and spacing for the small size
|
||||
.kbnFieldButton--small {
|
||||
.kbnFieldButton--xs {
|
||||
font-size: $euiFontSizeXS;
|
||||
|
||||
.kbnFieldButton__button {
|
||||
padding: $euiSizeXS;
|
||||
padding: $euiSizeXS 0;
|
||||
}
|
||||
|
||||
.kbnFieldButton__fieldIcon,
|
||||
.kbnFieldButton__fieldAction {
|
||||
margin-right: $euiSizeXS;
|
||||
margin-left: $euiSizeXS;
|
||||
}
|
||||
|
||||
.kbnFieldButton__fieldAction,
|
||||
.kbnFieldButton__dragHandle {
|
||||
line-height: $euiSizeL;
|
||||
}
|
||||
}
|
||||
|
||||
.kbnFieldButton--flushBoth {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
|
|
@ -21,9 +21,11 @@ describe('sizes', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('isDraggable', () => {
|
||||
describe('with drag handle', () => {
|
||||
it('is rendered', () => {
|
||||
const component = shallow(<FieldButton onClick={noop} fieldName="name" isDraggable />);
|
||||
const component = shallow(
|
||||
<FieldButton size="s" onClick={noop} fieldName="name" dragHandle={<span>drag</span>} />
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
@ -31,7 +33,7 @@ describe('isDraggable', () => {
|
|||
describe('fieldIcon', () => {
|
||||
it('is rendered', () => {
|
||||
const component = shallow(
|
||||
<FieldButton onClick={noop} fieldName="name" fieldIcon={<span>fieldIcon</span>} />
|
||||
<FieldButton size="s" onClick={noop} fieldName="name" fieldIcon={<span>fieldIcon</span>} />
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
@ -40,7 +42,12 @@ describe('fieldIcon', () => {
|
|||
describe('fieldAction', () => {
|
||||
it('is rendered', () => {
|
||||
const component = shallow(
|
||||
<FieldButton onClick={noop} fieldName="name" fieldAction={<span>fieldAction</span>} />
|
||||
<FieldButton
|
||||
size="s"
|
||||
onClick={noop}
|
||||
fieldName="name"
|
||||
fieldAction={<span>fieldAction</span>}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
@ -48,11 +55,11 @@ describe('fieldAction', () => {
|
|||
|
||||
describe('isActive', () => {
|
||||
it('defaults to false', () => {
|
||||
const component = shallow(<FieldButton onClick={noop} fieldName="name" />);
|
||||
const component = shallow(<FieldButton size="s" onClick={noop} fieldName="name" />);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
it('renders true', () => {
|
||||
const component = shallow(<FieldButton onClick={noop} fieldName="name" isActive />);
|
||||
const component = shallow(<FieldButton size="s" onClick={noop} fieldName="name" isActive />);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import './field_button.scss';
|
||||
import classNames from 'classnames';
|
||||
import React, { ReactNode, HTMLAttributes, ButtonHTMLAttributes } from 'react';
|
||||
import { CommonProps } from '@elastic/eui';
|
||||
import './field_button.scss';
|
||||
|
||||
export interface FieldButtonProps extends HTMLAttributes<HTMLDivElement> {
|
||||
/**
|
||||
|
@ -34,13 +34,20 @@ export interface FieldButtonProps extends HTMLAttributes<HTMLDivElement> {
|
|||
*/
|
||||
isActive?: boolean;
|
||||
/**
|
||||
* Styles the component differently to indicate it is draggable
|
||||
* Custom drag handle element
|
||||
*/
|
||||
isDraggable?: boolean;
|
||||
dragHandle?: React.ReactElement;
|
||||
/**
|
||||
* Use the small size in condensed areas
|
||||
* Use the xs size in condensed areas
|
||||
*/
|
||||
size: ButtonSize;
|
||||
/**
|
||||
* Whether to skip side paddings
|
||||
*/
|
||||
flush?: 'both';
|
||||
/**
|
||||
* Custom class name
|
||||
*/
|
||||
size?: ButtonSize;
|
||||
className?: string;
|
||||
/**
|
||||
* The component will render a `<button>` when provided an `onClick`
|
||||
|
@ -57,8 +64,8 @@ export interface FieldButtonProps extends HTMLAttributes<HTMLDivElement> {
|
|||
}
|
||||
|
||||
const sizeToClassNameMap = {
|
||||
s: 'kbnFieldButton--small',
|
||||
m: null,
|
||||
xs: 'kbnFieldButton--xs',
|
||||
s: 'kbnFieldButton--s',
|
||||
} as const;
|
||||
|
||||
export type ButtonSize = keyof typeof sizeToClassNameMap;
|
||||
|
@ -66,14 +73,15 @@ export type ButtonSize = keyof typeof sizeToClassNameMap;
|
|||
export const SIZES = Object.keys(sizeToClassNameMap) as ButtonSize[];
|
||||
|
||||
export function FieldButton({
|
||||
size = 'm',
|
||||
size,
|
||||
isActive = false,
|
||||
fieldIcon,
|
||||
fieldName,
|
||||
fieldInfoIcon,
|
||||
fieldAction,
|
||||
flush,
|
||||
className,
|
||||
isDraggable = false,
|
||||
dragHandle,
|
||||
onClick,
|
||||
dataTestSubj,
|
||||
buttonProps,
|
||||
|
@ -82,8 +90,10 @@ export function FieldButton({
|
|||
const classes = classNames(
|
||||
'kbnFieldButton',
|
||||
size ? sizeToClassNameMap[size] : null,
|
||||
{ 'kbnFieldButton-isActive': isActive },
|
||||
{ 'kbnFieldButton--isDraggable': isDraggable },
|
||||
{
|
||||
'kbnFieldButton-isActive': isActive,
|
||||
'kbnFieldButton--flushBoth': flush === 'both',
|
||||
},
|
||||
className
|
||||
);
|
||||
|
||||
|
@ -92,13 +102,18 @@ export function FieldButton({
|
|||
const innerContent = (
|
||||
<>
|
||||
{fieldIcon && <span className="kbnFieldButton__fieldIcon">{fieldIcon}</span>}
|
||||
{fieldName && <span className="kbnFieldButton__name">{fieldName}</span>}
|
||||
{fieldName && (
|
||||
<span className="kbnFieldButton__name">
|
||||
<span className="kbnFieldButton__nameInner">{fieldName}</span>
|
||||
</span>
|
||||
)}
|
||||
{fieldInfoIcon && <div className="kbnFieldButton__infoIcon">{fieldInfoIcon}</div>}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={classes} {...rest}>
|
||||
{dragHandle && <div className="kbnFieldButton__dragHandle">{dragHandle}</div>}
|
||||
{onClick ? (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
import './discover_layout.scss';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
|
@ -21,6 +21,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import classNames from 'classnames';
|
||||
import { generateFilters } from '@kbn/data-plugin/public';
|
||||
import { DragContext } from '@kbn/dom-drag-drop';
|
||||
import { DataView, DataViewField, DataViewType } from '@kbn/data-views-plugin/public';
|
||||
import { VIEW_MODE } from '../../../../../common/constants';
|
||||
import { useInternalStateSelector } from '../../services/discover_internal_state_container';
|
||||
|
@ -207,6 +208,17 @@ export function DiscoverLayout({
|
|||
|
||||
const resizeRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const dragDropContext = useContext(DragContext);
|
||||
const draggingFieldName = dragDropContext.dragging?.id;
|
||||
|
||||
const onDropFieldToTable = useMemo(() => {
|
||||
if (!draggingFieldName || currentColumns.includes(draggingFieldName)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return () => onAddColumn(draggingFieldName);
|
||||
}, [onAddColumn, draggingFieldName, currentColumns]);
|
||||
|
||||
const mainDisplay = useMemo(() => {
|
||||
if (resultState === 'none') {
|
||||
const globalQueryState = data.query.getState();
|
||||
|
@ -245,6 +257,7 @@ export function DiscoverLayout({
|
|||
onFieldEdited={onFieldEdited}
|
||||
resizeRef={resizeRef}
|
||||
inspectorAdapters={inspectorAdapters}
|
||||
onDropFieldToTable={onDropFieldToTable}
|
||||
/>
|
||||
{resultState === 'loading' && <LoadingSpinner />}
|
||||
</>
|
||||
|
@ -265,7 +278,9 @@ export function DiscoverLayout({
|
|||
savedSearch,
|
||||
stateContainer,
|
||||
viewMode,
|
||||
onDropFieldToTable,
|
||||
]);
|
||||
|
||||
return (
|
||||
<EuiPage className="dscPage" data-fetch-counter={fetchCounter.current}>
|
||||
<h1
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui';
|
||||
import { SavedSearch } from '@kbn/saved-search-plugin/public';
|
||||
import { DragDrop, type DropType, DropOverlayWrapper } from '@kbn/dom-drag-drop';
|
||||
import React, { useCallback } from 'react';
|
||||
import { DataView } from '@kbn/data-views-plugin/common';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
|
@ -23,6 +24,19 @@ import { DOCUMENTS_VIEW_CLICK, FIELD_STATISTICS_VIEW_CLICK } from '../field_stat
|
|||
import { ErrorCallout } from '../../../../components/common/error_callout';
|
||||
import { useDataState } from '../../hooks/use_data_state';
|
||||
|
||||
const DROP_PROPS = {
|
||||
value: {
|
||||
id: 'dscDropZoneTable',
|
||||
humanData: {
|
||||
label: i18n.translate('discover.dropZoneTableLabel', {
|
||||
defaultMessage: 'Drop zone to add field as a column to the table',
|
||||
}),
|
||||
},
|
||||
},
|
||||
order: [1, 0, 0, 0],
|
||||
types: ['field_add'] as DropType[],
|
||||
};
|
||||
|
||||
export interface DiscoverMainContentProps {
|
||||
dataView: DataView;
|
||||
savedSearch: SavedSearch;
|
||||
|
@ -32,6 +46,7 @@ export interface DiscoverMainContentProps {
|
|||
viewMode: VIEW_MODE;
|
||||
onAddFilter: DocViewFilterFn | undefined;
|
||||
onFieldEdited: () => Promise<void>;
|
||||
onDropFieldToTable?: () => void;
|
||||
columns: string[];
|
||||
}
|
||||
|
||||
|
@ -45,6 +60,7 @@ export const DiscoverMainContent = ({
|
|||
columns,
|
||||
stateContainer,
|
||||
savedSearch,
|
||||
onDropFieldToTable,
|
||||
}: DiscoverMainContentProps) => {
|
||||
const { trackUiMetric } = useDiscoverServices();
|
||||
|
||||
|
@ -64,49 +80,65 @@ export const DiscoverMainContent = ({
|
|||
);
|
||||
|
||||
const dataState = useDataState(stateContainer.dataState.data$.main$);
|
||||
const isDropAllowed = Boolean(onDropFieldToTable);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
className="eui-fullHeight"
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
responsive={false}
|
||||
<DragDrop
|
||||
draggable={false}
|
||||
dropTypes={isDropAllowed ? DROP_PROPS.types : undefined}
|
||||
value={DROP_PROPS.value}
|
||||
order={DROP_PROPS.order}
|
||||
onDrop={onDropFieldToTable}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiHorizontalRule margin="none" />
|
||||
{!isPlainRecord && (
|
||||
<DocumentViewModeToggle viewMode={viewMode} setDiscoverViewMode={setDiscoverViewMode} />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
{dataState.error && (
|
||||
<ErrorCallout
|
||||
title={i18n.translate('discover.documentsErrorTitle', {
|
||||
defaultMessage: 'Search error',
|
||||
})}
|
||||
error={dataState.error}
|
||||
inline
|
||||
data-test-subj="discoverMainError"
|
||||
/>
|
||||
)}
|
||||
{viewMode === VIEW_MODE.DOCUMENT_LEVEL ? (
|
||||
<DiscoverDocuments
|
||||
dataView={dataView}
|
||||
navigateTo={navigateTo}
|
||||
onAddFilter={!isPlainRecord ? onAddFilter : undefined}
|
||||
savedSearch={savedSearch}
|
||||
stateContainer={stateContainer}
|
||||
onFieldEdited={!isPlainRecord ? onFieldEdited : undefined}
|
||||
/>
|
||||
) : (
|
||||
<FieldStatisticsTab
|
||||
savedSearch={savedSearch}
|
||||
dataView={dataView}
|
||||
columns={columns}
|
||||
stateContainer={stateContainer}
|
||||
onAddFilter={!isPlainRecord ? onAddFilter : undefined}
|
||||
trackUiMetric={trackUiMetric}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
<DropOverlayWrapper isVisible={isDropAllowed}>
|
||||
<EuiFlexGroup
|
||||
className="eui-fullHeight"
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
responsive={false}
|
||||
data-test-subj="dscMainContent"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiHorizontalRule margin="none" />
|
||||
{!isPlainRecord && (
|
||||
<DocumentViewModeToggle
|
||||
viewMode={viewMode}
|
||||
setDiscoverViewMode={setDiscoverViewMode}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
{dataState.error && (
|
||||
<ErrorCallout
|
||||
title={i18n.translate('discover.documentsErrorTitle', {
|
||||
defaultMessage: 'Search error',
|
||||
})}
|
||||
error={dataState.error}
|
||||
inline
|
||||
data-test-subj="discoverMainError"
|
||||
/>
|
||||
)}
|
||||
|
||||
{viewMode === VIEW_MODE.DOCUMENT_LEVEL ? (
|
||||
<DiscoverDocuments
|
||||
dataView={dataView}
|
||||
navigateTo={navigateTo}
|
||||
onAddFilter={!isPlainRecord ? onAddFilter : undefined}
|
||||
savedSearch={savedSearch}
|
||||
stateContainer={stateContainer}
|
||||
onFieldEdited={!isPlainRecord ? onFieldEdited : undefined}
|
||||
/>
|
||||
) : (
|
||||
<FieldStatisticsTab
|
||||
savedSearch={savedSearch}
|
||||
dataView={dataView}
|
||||
columns={columns}
|
||||
stateContainer={stateContainer}
|
||||
onAddFilter={!isPlainRecord ? onAddFilter : undefined}
|
||||
trackUiMetric={trackUiMetric}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</DropOverlayWrapper>
|
||||
</DragDrop>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { EuiPopover, EuiProgress, EuiButtonIcon } from '@elastic/eui';
|
||||
import { EuiButtonIcon, EuiPopover, EuiProgress } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
|
@ -23,6 +23,7 @@ import { DataDocuments$ } from '../../services/discover_data_state_container';
|
|||
import { getDataTableRecords } from '../../../../__fixtures__/real_hits';
|
||||
import * as DetailsUtil from './deprecated_stats/get_details';
|
||||
import { createDiscoverServicesMock } from '../../../../__mocks__/services';
|
||||
import { FieldItemButton } from '@kbn/unified-field-list-plugin/public';
|
||||
|
||||
jest.spyOn(DetailsUtil, 'getDetails');
|
||||
|
||||
|
@ -97,7 +98,10 @@ async function getComponent({
|
|||
onEditField: jest.fn(),
|
||||
onRemoveField: jest.fn(),
|
||||
showFieldStats,
|
||||
selected,
|
||||
isSelected: selected,
|
||||
isEmpty: false,
|
||||
groupIndex: 1,
|
||||
itemIndex: 0,
|
||||
contextualFields: [],
|
||||
};
|
||||
const services = {
|
||||
|
@ -206,7 +210,7 @@ describe('discover sidebar field', function () {
|
|||
findTestSubject(comp, 'field-bytes-showDetails').simulate('click');
|
||||
expect(DetailsUtil.getDetails).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('should not return the popover if onAddFilter is not provided', async function () {
|
||||
it('should not enable the popover if onAddFilter is not provided', async function () {
|
||||
const field = new DataViewField({
|
||||
name: '_source',
|
||||
type: '_source',
|
||||
|
@ -220,8 +224,8 @@ describe('discover sidebar field', function () {
|
|||
field,
|
||||
onAddFilterExists: false,
|
||||
});
|
||||
const popover = findTestSubject(comp, 'discoverFieldListPanelPopover');
|
||||
expect(popover.length).toBe(0);
|
||||
|
||||
expect(comp.find(FieldItemButton).prop('onClick')).toBeUndefined();
|
||||
});
|
||||
it('should request field stats', async function () {
|
||||
const field = new DataViewField({
|
||||
|
|
|
@ -8,162 +8,69 @@
|
|||
|
||||
import './discover_field.scss';
|
||||
|
||||
import React, { useState, useCallback, memo, useMemo } from 'react';
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiToolTip,
|
||||
EuiTitle,
|
||||
EuiIcon,
|
||||
EuiSpacer,
|
||||
EuiHighlight,
|
||||
} from '@elastic/eui';
|
||||
import React, { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { UiCounterMetricType } from '@kbn/analytics';
|
||||
import classNames from 'classnames';
|
||||
import { FieldButton } from '@kbn/react-field';
|
||||
import type { DataViewField, DataView } from '@kbn/data-views-plugin/public';
|
||||
import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
|
||||
import {
|
||||
FieldIcon,
|
||||
FieldItemButton,
|
||||
type FieldItemButtonProps,
|
||||
FieldPopover,
|
||||
FieldPopoverHeader,
|
||||
FieldPopoverHeaderProps,
|
||||
FieldPopoverVisualize,
|
||||
getFieldIconProps,
|
||||
} from '@kbn/unified-field-list-plugin/public';
|
||||
import { DragDrop } from '@kbn/dom-drag-drop';
|
||||
import { DiscoverFieldStats } from './discover_field_stats';
|
||||
import { DiscoverFieldDetails } from './deprecated_stats/discover_field_details';
|
||||
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
|
||||
import { SHOW_LEGACY_FIELD_TOP_VALUES, PLUGIN_ID } from '../../../../../common';
|
||||
import { PLUGIN_ID, SHOW_LEGACY_FIELD_TOP_VALUES } from '../../../../../common';
|
||||
import { getUiActions } from '../../../../kibana_services';
|
||||
import { type DataDocuments$ } from '../../services/discover_data_state_container';
|
||||
|
||||
const FieldInfoIcon: React.FC = memo(() => (
|
||||
<EuiToolTip
|
||||
position="bottom"
|
||||
content={i18n.translate('discover.field.mappingConflict', {
|
||||
defaultMessage:
|
||||
'This field is defined as several types (string, integer, etc) across the indices that match this pattern.' +
|
||||
'You may still be able to use this conflicting field, but it will be unavailable for functions that require Kibana to know their type. Correcting this issue will require reindexing your data.',
|
||||
})}
|
||||
>
|
||||
<EuiIcon
|
||||
tabIndex={0}
|
||||
type="warning"
|
||||
title={i18n.translate('discover.field.mappingConflict.title', {
|
||||
defaultMessage: 'Mapping Conflict',
|
||||
})}
|
||||
size="s"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
));
|
||||
|
||||
const DiscoverFieldTypeIcon: React.FC<{ field: DataViewField }> = memo(({ field }) => {
|
||||
return <FieldIcon {...getFieldIconProps(field)} />;
|
||||
});
|
||||
|
||||
const FieldName: React.FC<{ field: DataViewField; highlight?: string }> = memo(
|
||||
({ field, highlight }) => {
|
||||
const title =
|
||||
field.displayName !== field.name
|
||||
? i18n.translate('discover.field.title', {
|
||||
defaultMessage: '{fieldName} ({fieldDisplayName})',
|
||||
values: {
|
||||
fieldName: field.name,
|
||||
fieldDisplayName: field.displayName,
|
||||
},
|
||||
})
|
||||
: field.displayName;
|
||||
|
||||
return (
|
||||
<EuiHighlight
|
||||
search={highlight || ''}
|
||||
data-test-subj={`field-${field.name}`}
|
||||
title={title}
|
||||
className="dscSidebarField__name"
|
||||
>
|
||||
{field.displayName}
|
||||
</EuiHighlight>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
interface ActionButtonProps {
|
||||
interface GetCommonFieldItemButtonPropsParams {
|
||||
field: DataViewField;
|
||||
isSelected?: boolean;
|
||||
alwaysShow: boolean;
|
||||
isSelected: boolean;
|
||||
toggleDisplay: (field: DataViewField, isSelected?: boolean) => void;
|
||||
}
|
||||
|
||||
const ActionButton: React.FC<ActionButtonProps> = memo(
|
||||
({ field, isSelected, alwaysShow, toggleDisplay }) => {
|
||||
const actionBtnClassName = classNames('dscSidebarItem__action', {
|
||||
['dscSidebarItem__mobile']: alwaysShow,
|
||||
});
|
||||
if (field.name === '_source') {
|
||||
return null;
|
||||
}
|
||||
if (!isSelected) {
|
||||
return (
|
||||
<EuiToolTip
|
||||
delay="long"
|
||||
content={i18n.translate('discover.fieldChooser.discoverField.addFieldTooltip', {
|
||||
defaultMessage: 'Add field as column',
|
||||
})}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
iconType="plusInCircleFilled"
|
||||
className={actionBtnClassName}
|
||||
onClick={(ev: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (ev.type === 'click') {
|
||||
ev.currentTarget.focus();
|
||||
}
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
toggleDisplay(field, isSelected);
|
||||
}}
|
||||
data-test-subj={`fieldToggle-${field.name}`}
|
||||
aria-label={i18n.translate('discover.fieldChooser.discoverField.addButtonAriaLabel', {
|
||||
defaultMessage: 'Add {field} to table',
|
||||
values: { field: field.name },
|
||||
})}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<EuiToolTip
|
||||
key={`tooltip-${field.name}-${field.count || 0}-${isSelected}`}
|
||||
delay="long"
|
||||
content={i18n.translate('discover.fieldChooser.discoverField.removeFieldTooltip', {
|
||||
defaultMessage: 'Remove field from table',
|
||||
})}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
color="danger"
|
||||
iconType="cross"
|
||||
className={actionBtnClassName}
|
||||
onClick={(ev: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (ev.type === 'click') {
|
||||
ev.currentTarget.focus();
|
||||
}
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
toggleDisplay(field, isSelected);
|
||||
}}
|
||||
data-test-subj={`fieldToggle-${field.name}`}
|
||||
aria-label={i18n.translate(
|
||||
'discover.fieldChooser.discoverField.removeButtonAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Remove {field} from table',
|
||||
values: { field: field.name },
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
function getCommonFieldItemButtonProps({
|
||||
field,
|
||||
isSelected,
|
||||
toggleDisplay,
|
||||
}: GetCommonFieldItemButtonPropsParams): {
|
||||
field: FieldItemButtonProps<DataViewField>['field'];
|
||||
isSelected: FieldItemButtonProps<DataViewField>['isSelected'];
|
||||
dataTestSubj: FieldItemButtonProps<DataViewField>['dataTestSubj'];
|
||||
buttonAddFieldToWorkspaceProps: FieldItemButtonProps<DataViewField>['buttonAddFieldToWorkspaceProps'];
|
||||
buttonRemoveFieldFromWorkspaceProps: FieldItemButtonProps<DataViewField>['buttonRemoveFieldFromWorkspaceProps'];
|
||||
onAddFieldToWorkspace: FieldItemButtonProps<DataViewField>['onAddFieldToWorkspace'];
|
||||
onRemoveFieldFromWorkspace: FieldItemButtonProps<DataViewField>['onRemoveFieldFromWorkspace'];
|
||||
} {
|
||||
const dataTestSubj = `fieldToggle-${field.name}`;
|
||||
const handler =
|
||||
field.name === '_source' ? undefined : (f: DataViewField) => toggleDisplay(f, isSelected);
|
||||
return {
|
||||
field,
|
||||
isSelected,
|
||||
dataTestSubj: `field-${field.name}-showDetails`,
|
||||
buttonAddFieldToWorkspaceProps: {
|
||||
'aria-label': i18n.translate('discover.fieldChooser.discoverField.addFieldTooltip', {
|
||||
defaultMessage: 'Add field as column',
|
||||
}),
|
||||
'data-test-subj': dataTestSubj,
|
||||
},
|
||||
buttonRemoveFieldFromWorkspaceProps: {
|
||||
'aria-label': i18n.translate('discover.fieldChooser.discoverField.removeFieldTooltip', {
|
||||
defaultMessage: 'Remove field from table',
|
||||
}),
|
||||
'data-test-subj': dataTestSubj,
|
||||
},
|
||||
onAddFieldToWorkspace: handler,
|
||||
onRemoveFieldFromWorkspace: handler,
|
||||
};
|
||||
}
|
||||
|
||||
interface MultiFieldsProps {
|
||||
multiFields: NonNullable<DiscoverFieldProps['multiFields']>;
|
||||
|
@ -183,22 +90,20 @@ const MultiFields: React.FC<MultiFieldsProps> = memo(
|
|||
</EuiTitle>
|
||||
<EuiSpacer size="xs" />
|
||||
{multiFields.map((entry) => (
|
||||
<FieldButton
|
||||
size="s"
|
||||
className="dscSidebarItem dscSidebarItem--multi"
|
||||
isActive={false}
|
||||
dataTestSubj={`field-${entry.field.name}-showDetails`}
|
||||
fieldIcon={<DiscoverFieldTypeIcon field={entry.field} />}
|
||||
fieldAction={
|
||||
<ActionButton
|
||||
field={entry.field}
|
||||
isSelected={entry.isSelected}
|
||||
alwaysShow={alwaysShowActionButton}
|
||||
toggleDisplay={toggleDisplay}
|
||||
/>
|
||||
}
|
||||
fieldName={<FieldName field={entry.field} />}
|
||||
<FieldItemButton
|
||||
key={entry.field.name}
|
||||
size="xs"
|
||||
className="dscSidebarItem dscSidebarItem--multi"
|
||||
flush="both"
|
||||
isEmpty={false}
|
||||
isActive={false}
|
||||
shouldAlwaysShowAction={alwaysShowActionButton}
|
||||
onClick={undefined}
|
||||
{...getCommonFieldItemButtonProps({
|
||||
field: entry.field,
|
||||
isSelected: entry.isSelected,
|
||||
toggleDisplay,
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
</React.Fragment>
|
||||
|
@ -235,10 +140,14 @@ export interface DiscoverFieldProps {
|
|||
* @param fieldName
|
||||
*/
|
||||
onRemoveField: (fieldName: string) => void;
|
||||
/**
|
||||
* Determines whether the field is empty
|
||||
*/
|
||||
isEmpty: boolean;
|
||||
/**
|
||||
* Determines whether the field is selected
|
||||
*/
|
||||
selected?: boolean;
|
||||
isSelected: boolean;
|
||||
/**
|
||||
* Metric tracking function
|
||||
* @param metricType
|
||||
|
@ -273,6 +182,16 @@ export interface DiscoverFieldProps {
|
|||
* Search by field name
|
||||
*/
|
||||
highlight?: string;
|
||||
|
||||
/**
|
||||
* Group index in the field list
|
||||
*/
|
||||
groupIndex: number;
|
||||
|
||||
/**
|
||||
* Item index in the field list
|
||||
*/
|
||||
itemIndex: number;
|
||||
}
|
||||
|
||||
function DiscoverFieldComponent({
|
||||
|
@ -284,13 +203,16 @@ function DiscoverFieldComponent({
|
|||
onAddField,
|
||||
onRemoveField,
|
||||
onAddFilter,
|
||||
selected,
|
||||
isEmpty,
|
||||
isSelected,
|
||||
trackUiMetric,
|
||||
multiFields,
|
||||
onEditField,
|
||||
onDeleteField,
|
||||
showFieldStats,
|
||||
contextualFields,
|
||||
groupIndex,
|
||||
itemIndex,
|
||||
}: DiscoverFieldProps) {
|
||||
const services = useDiscoverServices();
|
||||
const [infoIsOpen, setOpen] = useState(false);
|
||||
|
@ -315,7 +237,7 @@ function DiscoverFieldComponent({
|
|||
setOpen(false);
|
||||
}, [setOpen]);
|
||||
|
||||
const toggleDisplay: ActionButtonProps['toggleDisplay'] = useCallback(
|
||||
const toggleDisplay: GetCommonFieldItemButtonPropsParams['toggleDisplay'] = useCallback(
|
||||
(f, isCurrentlySelected) => {
|
||||
closePopover();
|
||||
if (isCurrentlySelected) {
|
||||
|
@ -349,51 +271,6 @@ function DiscoverFieldComponent({
|
|||
[field.name]
|
||||
);
|
||||
|
||||
if (field.type === '_source') {
|
||||
return (
|
||||
<FieldButton
|
||||
size="s"
|
||||
className="dscSidebarItem"
|
||||
dataTestSubj={`field-${field.name}-showDetails`}
|
||||
fieldIcon={<DiscoverFieldTypeIcon field={field} />}
|
||||
fieldAction={
|
||||
<ActionButton
|
||||
field={field}
|
||||
isSelected={selected}
|
||||
alwaysShow={alwaysShowActionButton}
|
||||
toggleDisplay={toggleDisplay}
|
||||
/>
|
||||
}
|
||||
fieldName={<FieldName field={field} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const button = (
|
||||
<FieldButton
|
||||
size="s"
|
||||
className="dscSidebarItem"
|
||||
isActive={infoIsOpen}
|
||||
onClick={isDocumentRecord ? togglePopover : undefined}
|
||||
dataTestSubj={`field-${field.name}-showDetails`}
|
||||
fieldIcon={<DiscoverFieldTypeIcon field={field} />}
|
||||
fieldAction={
|
||||
<ActionButton
|
||||
field={field}
|
||||
isSelected={selected}
|
||||
alwaysShow={alwaysShowActionButton}
|
||||
toggleDisplay={toggleDisplay}
|
||||
/>
|
||||
}
|
||||
fieldName={<FieldName field={field} highlight={highlight} />}
|
||||
fieldInfoIcon={field.type === 'conflict' && <FieldInfoIcon />}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!isDocumentRecord) {
|
||||
return button;
|
||||
}
|
||||
|
||||
const renderPopover = () => {
|
||||
const showLegacyFieldStats = services.uiSettings.get(SHOW_LEGACY_FIELD_TOP_VALUES);
|
||||
|
||||
|
@ -443,24 +320,57 @@ function DiscoverFieldComponent({
|
|||
);
|
||||
};
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
id: field.name,
|
||||
humanData: {
|
||||
label: field.displayName,
|
||||
position: itemIndex + 1,
|
||||
},
|
||||
}),
|
||||
[field, itemIndex]
|
||||
);
|
||||
const order = useMemo(() => [0, groupIndex, itemIndex], [groupIndex, itemIndex]);
|
||||
|
||||
return (
|
||||
<FieldPopover
|
||||
isOpen={infoIsOpen}
|
||||
button={button}
|
||||
button={
|
||||
<DragDrop
|
||||
draggable
|
||||
order={order}
|
||||
value={value}
|
||||
onDragStart={closePopover}
|
||||
isDisabled={alwaysShowActionButton}
|
||||
dataTestSubj={`dscFieldListPanelField-${field.name}`}
|
||||
>
|
||||
<FieldItemButton
|
||||
size="xs"
|
||||
fieldSearchHighlight={highlight}
|
||||
className="dscSidebarItem"
|
||||
isEmpty={isEmpty}
|
||||
isActive={infoIsOpen}
|
||||
flush={alwaysShowActionButton ? 'both' : undefined}
|
||||
shouldAlwaysShowAction={alwaysShowActionButton}
|
||||
onClick={field.type !== '_source' ? togglePopover : undefined}
|
||||
{...getCommonFieldItemButtonProps({ field, isSelected, toggleDisplay })}
|
||||
/>
|
||||
</DragDrop>
|
||||
}
|
||||
closePopover={closePopover}
|
||||
data-test-subj="discoverFieldListPanelPopover"
|
||||
renderHeader={() => (
|
||||
<FieldPopoverHeader
|
||||
field={field}
|
||||
closePopover={closePopover}
|
||||
onAddFieldToWorkspace={!selected ? toggleDisplay : undefined}
|
||||
onAddFieldToWorkspace={!isSelected ? toggleDisplay : undefined}
|
||||
onAddFilter={onAddFilter}
|
||||
onEditField={onEditField}
|
||||
onDeleteField={onDeleteField}
|
||||
{...customPopoverHeaderProps}
|
||||
/>
|
||||
)}
|
||||
renderContent={renderPopover}
|
||||
renderContent={isDocumentRecord ? renderPopover : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -38,29 +38,3 @@
|
|||
.dscSidebar__flyoutHeader {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dscSidebarItem {
|
||||
&:hover,
|
||||
&:focus-within,
|
||||
&[class*='-isActive'] {
|
||||
.dscSidebarItem__action {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Only visually hide the action, so that it's still accessible to screen readers.
|
||||
* 2. When tabbed to, this element needs to be visible for keyboard accessibility.
|
||||
*/
|
||||
.dscSidebarItem__action {
|
||||
opacity: 0; /* 1 */
|
||||
|
||||
&.dscSidebarItem__mobile {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
opacity: 1; /* 2 */
|
||||
}
|
||||
}
|
||||
|
|
|
@ -223,7 +223,7 @@ export function DiscoverSidebarComponent({
|
|||
});
|
||||
|
||||
const renderFieldItem: FieldListGroupedProps<DataViewField>['renderFieldItem'] = useCallback(
|
||||
({ field, groupName, fieldSearchHighlight }) => (
|
||||
({ field, groupName, groupIndex, itemIndex, fieldSearchHighlight }) => (
|
||||
<li key={`field${field.name}`} data-attr-field={field.name}>
|
||||
<DiscoverField
|
||||
alwaysShowActionButton={alwaysShowActionButtons}
|
||||
|
@ -240,7 +240,10 @@ export function DiscoverSidebarComponent({
|
|||
onDeleteField={deleteField}
|
||||
showFieldStats={showFieldStats}
|
||||
contextualFields={columns}
|
||||
selected={
|
||||
groupIndex={groupIndex}
|
||||
itemIndex={itemIndex}
|
||||
isEmpty={groupName === FieldsGroupNames.EmptyFields}
|
||||
isSelected={
|
||||
groupName === FieldsGroupNames.SelectedFields ||
|
||||
Boolean(selectedFieldsState.selectedFieldsMap[field.name])
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { RootDragDropProvider } from '@kbn/dom-drag-drop';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { SavedSearch } from '@kbn/saved-search-plugin/public';
|
||||
import { DataViewListItem } from '@kbn/data-views-plugin/public';
|
||||
|
@ -102,19 +103,21 @@ export function DiscoverMainApp(props: DiscoverMainProps) {
|
|||
|
||||
return (
|
||||
<DiscoverMainProvider value={stateContainer}>
|
||||
<DiscoverLayoutMemoized
|
||||
inspectorAdapters={inspectorAdapters}
|
||||
onChangeDataView={onChangeDataView}
|
||||
onUpdateQuery={onUpdateQuery}
|
||||
resetSavedSearch={resetCurrentSavedSearch}
|
||||
navigateTo={navigateTo}
|
||||
savedSearch={savedSearch}
|
||||
searchSource={searchSource}
|
||||
stateContainer={stateContainer}
|
||||
persistDataView={persistDataView}
|
||||
updateAdHocDataViewId={updateAdHocDataViewId}
|
||||
updateDataViewList={updateDataViewList}
|
||||
/>
|
||||
<RootDragDropProvider>
|
||||
<DiscoverLayoutMemoized
|
||||
inspectorAdapters={inspectorAdapters}
|
||||
onChangeDataView={onChangeDataView}
|
||||
onUpdateQuery={onUpdateQuery}
|
||||
resetSavedSearch={resetCurrentSavedSearch}
|
||||
navigateTo={navigateTo}
|
||||
savedSearch={savedSearch}
|
||||
searchSource={searchSource}
|
||||
stateContainer={stateContainer}
|
||||
persistDataView={persistDataView}
|
||||
updateAdHocDataViewId={updateAdHocDataViewId}
|
||||
updateDataViewList={updateDataViewList}
|
||||
/>
|
||||
</RootDragDropProvider>
|
||||
</DiscoverMainProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
"@kbn/config-schema",
|
||||
"@kbn/storybook",
|
||||
"@kbn/shared-ux-router",
|
||||
"@kbn/dom-drag-drop",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -0,0 +1,278 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`UnifiedFieldList <FieldItemButton /> renders properly 1`] = `
|
||||
<FieldButton
|
||||
buttonProps={
|
||||
Object {
|
||||
"aria-label": "Preview bytes: number",
|
||||
}
|
||||
}
|
||||
className="unifiedFieldListItemButton unifiedFieldListItemButton--number unifiedFieldListItemButton--exists"
|
||||
fieldIcon={
|
||||
<WrappedFieldIcon
|
||||
scripted={false}
|
||||
type="number"
|
||||
/>
|
||||
}
|
||||
fieldName={
|
||||
<EuiHighlight
|
||||
data-test-subj="field-bytes"
|
||||
search="by"
|
||||
title="bytes"
|
||||
>
|
||||
bytes
|
||||
</EuiHighlight>
|
||||
}
|
||||
isActive={false}
|
||||
key="field-item-button-bytes"
|
||||
onClick={[MockFunction]}
|
||||
size="s"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`UnifiedFieldList <FieldItemButton /> renders properly for Records (Lens field) 1`] = `
|
||||
<FieldButton
|
||||
buttonProps={
|
||||
Object {
|
||||
"aria-label": "Preview Records: document",
|
||||
}
|
||||
}
|
||||
className="unifiedFieldListItemButton unifiedFieldListItemButton--document unifiedFieldListItemButton--exists"
|
||||
fieldIcon={
|
||||
<WrappedFieldIcon
|
||||
scripted={false}
|
||||
type="document"
|
||||
/>
|
||||
}
|
||||
fieldName={
|
||||
<EuiHighlight
|
||||
data-test-subj="field-___records___"
|
||||
search="re"
|
||||
title="Records"
|
||||
>
|
||||
Records
|
||||
</EuiHighlight>
|
||||
}
|
||||
isActive={true}
|
||||
key="field-item-button-___records___"
|
||||
onClick={[MockFunction]}
|
||||
size="s"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`UnifiedFieldList <FieldItemButton /> renders properly for text-based column field 1`] = `
|
||||
<FieldButton
|
||||
buttonProps={
|
||||
Object {
|
||||
"aria-label": "Preview agent: string",
|
||||
}
|
||||
}
|
||||
className="unifiedFieldListItemButton unifiedFieldListItemButton--string unifiedFieldListItemButton--exists"
|
||||
fieldIcon={
|
||||
<WrappedFieldIcon
|
||||
type="string"
|
||||
/>
|
||||
}
|
||||
fieldName={
|
||||
<EuiHighlight
|
||||
data-test-subj="field-agent"
|
||||
search="ag"
|
||||
title="agent"
|
||||
>
|
||||
agent
|
||||
</EuiHighlight>
|
||||
}
|
||||
isActive={false}
|
||||
key="field-item-button-agent"
|
||||
size="s"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`UnifiedFieldList <FieldItemButton /> renders properly when a conflict field 1`] = `
|
||||
<FieldButton
|
||||
buttonProps={
|
||||
Object {
|
||||
"aria-label": "Preview custom_user_field: conflict",
|
||||
}
|
||||
}
|
||||
className="unifiedFieldListItemButton unifiedFieldListItemButton--conflict unifiedFieldListItemButton--exists"
|
||||
fieldIcon={
|
||||
<WrappedFieldIcon
|
||||
scripted={false}
|
||||
type="conflict"
|
||||
/>
|
||||
}
|
||||
fieldInfoIcon={<FieldConflictInfoIcon />}
|
||||
fieldName={
|
||||
<EuiHighlight
|
||||
data-test-subj="field-custom_user_field"
|
||||
search=""
|
||||
title="custom_user_field"
|
||||
>
|
||||
custom_user_field
|
||||
</EuiHighlight>
|
||||
}
|
||||
isActive={true}
|
||||
key="field-item-button-custom_user_field"
|
||||
onClick={[MockFunction]}
|
||||
size="s"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`UnifiedFieldList <FieldItemButton /> renders properly when empty 1`] = `
|
||||
<FieldButton
|
||||
buttonProps={
|
||||
Object {
|
||||
"aria-label": "Preview script date: date",
|
||||
}
|
||||
}
|
||||
className="unifiedFieldListItemButton unifiedFieldListItemButton--date unifiedFieldListItemButton--missing"
|
||||
fieldIcon={
|
||||
<WrappedFieldIcon
|
||||
scripted={true}
|
||||
type="date"
|
||||
/>
|
||||
}
|
||||
fieldName={
|
||||
<EuiHighlight
|
||||
data-test-subj="field-script date"
|
||||
search=""
|
||||
title="script date"
|
||||
>
|
||||
script date
|
||||
</EuiHighlight>
|
||||
}
|
||||
isActive={false}
|
||||
key="field-item-button-script date"
|
||||
onClick={[MockFunction]}
|
||||
size="s"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`UnifiedFieldList <FieldItemButton /> renders properly with a drag handle 1`] = `
|
||||
<FieldButton
|
||||
buttonProps={
|
||||
Object {
|
||||
"aria-label": "Preview bytes: number",
|
||||
}
|
||||
}
|
||||
className="unifiedFieldListItemButton unifiedFieldListItemButton--number unifiedFieldListItemButton--exists custom"
|
||||
dataTestSubj="test-subj"
|
||||
dragHandle={
|
||||
<span>
|
||||
dragHandle
|
||||
</span>
|
||||
}
|
||||
fieldIcon={
|
||||
<WrappedFieldIcon
|
||||
scripted={false}
|
||||
type="number"
|
||||
/>
|
||||
}
|
||||
fieldName={
|
||||
<EuiHighlight
|
||||
data-test-subj="field-bytes"
|
||||
search=""
|
||||
title="bytes"
|
||||
>
|
||||
bytes
|
||||
</EuiHighlight>
|
||||
}
|
||||
isActive={false}
|
||||
key="field-item-button-bytes"
|
||||
size="xs"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`UnifiedFieldList <FieldItemButton /> renders properly with an action when deselected 1`] = `
|
||||
<FieldButton
|
||||
buttonProps={
|
||||
Object {
|
||||
"aria-label": "Preview bytes: number",
|
||||
}
|
||||
}
|
||||
className="unifiedFieldListItemButton unifiedFieldListItemButton--number unifiedFieldListItemButton--exists"
|
||||
fieldAction={
|
||||
<EuiToolTip
|
||||
content="Add \\"bytes\\" field"
|
||||
delay="regular"
|
||||
display="inlineBlock"
|
||||
position="top"
|
||||
>
|
||||
<EuiButtonIcon
|
||||
aria-label="Add \\"bytes\\" field"
|
||||
className="unifiedFieldListItemButton__action unifiedFieldListItemButton__action--always"
|
||||
color="text"
|
||||
data-test-subj="unifiedFieldListItem_addField-bytes"
|
||||
iconType="plusInCircle"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
}
|
||||
fieldIcon={
|
||||
<WrappedFieldIcon
|
||||
scripted={false}
|
||||
type="number"
|
||||
/>
|
||||
}
|
||||
fieldName={
|
||||
<EuiHighlight
|
||||
data-test-subj="field-bytes"
|
||||
search=""
|
||||
title="bytes"
|
||||
>
|
||||
bytes
|
||||
</EuiHighlight>
|
||||
}
|
||||
isActive={false}
|
||||
key="field-item-button-bytes"
|
||||
size="s"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`UnifiedFieldList <FieldItemButton /> renders properly with an action when selected 1`] = `
|
||||
<FieldButton
|
||||
buttonProps={
|
||||
Object {
|
||||
"aria-label": "Preview bytes: number",
|
||||
}
|
||||
}
|
||||
className="unifiedFieldListItemButton unifiedFieldListItemButton--number unifiedFieldListItemButton--exists"
|
||||
fieldAction={
|
||||
<EuiToolTip
|
||||
content="Remove \\"bytes\\" field"
|
||||
delay="regular"
|
||||
display="inlineBlock"
|
||||
position="top"
|
||||
>
|
||||
<EuiButtonIcon
|
||||
aria-label="Remove \\"bytes\\" field"
|
||||
className="unifiedFieldListItemButton__action"
|
||||
color="danger"
|
||||
data-test-subj="unifiedFieldListItem_removeField-bytes"
|
||||
iconType="cross"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
}
|
||||
fieldIcon={
|
||||
<WrappedFieldIcon
|
||||
scripted={false}
|
||||
type="number"
|
||||
/>
|
||||
}
|
||||
fieldName={
|
||||
<EuiHighlight
|
||||
data-test-subj="field-bytes"
|
||||
search=""
|
||||
title="bytes"
|
||||
>
|
||||
bytes
|
||||
</EuiHighlight>
|
||||
}
|
||||
isActive={false}
|
||||
key="field-item-button-bytes"
|
||||
onClick={[MockFunction click]}
|
||||
size="s"
|
||||
/>
|
||||
`;
|
|
@ -24,13 +24,25 @@
|
|||
}
|
||||
}
|
||||
|
||||
.unifiedFieldItemButton {
|
||||
width: 100%;
|
||||
/**
|
||||
* 1. Only visually hide the action, so that it's still accessible to screen readers.
|
||||
* 2. When tabbed to, this element needs to be visible for keyboard accessibility.
|
||||
*/
|
||||
.unifiedFieldListItemButton__action {
|
||||
opacity: 0; /* 1 */
|
||||
|
||||
&:hover:not([class*='isActive']) {
|
||||
cursor: grab;
|
||||
&--always {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
opacity: 1; /* 2 */
|
||||
}
|
||||
}
|
||||
|
||||
.unifiedFieldListItemButton {
|
||||
width: 100%;
|
||||
|
||||
&.kbnFieldButton {
|
||||
&:focus-within,
|
||||
&-isActive {
|
||||
|
@ -39,15 +51,22 @@
|
|||
}
|
||||
|
||||
.kbnFieldButton__button:focus {
|
||||
@include passDownFocusRing('.kbnFieldButton__name > span');
|
||||
@include passDownFocusRing('.kbnFieldButton__nameInner');
|
||||
}
|
||||
|
||||
.kbnFieldButton__name > span {
|
||||
text-decoration: underline;
|
||||
& button .kbnFieldButton__nameInner:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&[class*='-isActive'] {
|
||||
.unifiedFieldListItemButton__action {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.unifiedFieldItemButton--missing {
|
||||
.unifiedFieldListItemButton--missing {
|
||||
background: lightOrDarkTheme(transparentize($euiColorMediumShade, .9), $euiColorEmptyShade);
|
||||
color: $euiColorDarkShade;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { DataViewField } from '@kbn/data-views-plugin/common';
|
||||
import { DatatableColumn } from '@kbn/expressions-plugin/common';
|
||||
import { stubLogstashDataView as dataView } from '@kbn/data-views-plugin/common/data_view.stub';
|
||||
import { FieldItemButton } from './field_item_button';
|
||||
|
||||
const bytesField = dataView.getFieldByName('bytes')!;
|
||||
const scriptedField = dataView.getFieldByName('script date')!;
|
||||
const conflictField = dataView.getFieldByName('custom_user_field')!;
|
||||
|
||||
describe('UnifiedFieldList <FieldItemButton />', () => {
|
||||
test('renders properly', () => {
|
||||
const component = shallow(
|
||||
<FieldItemButton
|
||||
field={bytesField}
|
||||
fieldSearchHighlight="by"
|
||||
isEmpty={false}
|
||||
isSelected={false}
|
||||
isActive={false}
|
||||
onClick={jest.fn()}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('renders properly when empty', () => {
|
||||
const component = shallow(
|
||||
<FieldItemButton
|
||||
field={scriptedField}
|
||||
fieldSearchHighlight={undefined}
|
||||
isEmpty={true}
|
||||
isSelected={true}
|
||||
isActive={false}
|
||||
onClick={jest.fn()}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('renders properly when a conflict field', () => {
|
||||
const component = shallow(
|
||||
<FieldItemButton
|
||||
field={conflictField}
|
||||
fieldSearchHighlight={undefined}
|
||||
isEmpty={false}
|
||||
isSelected={false}
|
||||
isActive={true}
|
||||
onClick={jest.fn()}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('renders properly for Records (Lens field)', () => {
|
||||
const component = shallow(
|
||||
<FieldItemButton
|
||||
field={
|
||||
new DataViewField({
|
||||
name: '___records___',
|
||||
customLabel: 'Records',
|
||||
type: 'document',
|
||||
searchable: false,
|
||||
aggregatable: true,
|
||||
})
|
||||
}
|
||||
fieldSearchHighlight="re"
|
||||
isEmpty={false}
|
||||
isSelected={false}
|
||||
isActive={true}
|
||||
onClick={jest.fn()}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('renders properly with an action when selected', () => {
|
||||
const component = shallow(
|
||||
<FieldItemButton
|
||||
field={bytesField}
|
||||
fieldSearchHighlight={undefined}
|
||||
isEmpty={false}
|
||||
isSelected={true}
|
||||
isActive={false}
|
||||
onClick={jest.fn().mockName('click')}
|
||||
onAddFieldToWorkspace={jest.fn().mockName('add')}
|
||||
onRemoveFieldFromWorkspace={jest.fn().mockName('remove')}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('renders properly with an action when deselected', () => {
|
||||
const component = shallow(
|
||||
<FieldItemButton
|
||||
field={bytesField}
|
||||
fieldSearchHighlight={undefined}
|
||||
isEmpty={false}
|
||||
isSelected={false}
|
||||
isActive={false}
|
||||
onClick={undefined}
|
||||
shouldAlwaysShowAction
|
||||
onAddFieldToWorkspace={jest.fn().mockName('add')}
|
||||
onRemoveFieldFromWorkspace={jest.fn().mockName('remove')}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('renders properly with a drag handle', () => {
|
||||
const component = shallow(
|
||||
<FieldItemButton
|
||||
size="xs"
|
||||
className="custom"
|
||||
dataTestSubj="test-subj"
|
||||
dragHandle={<span>dragHandle</span>}
|
||||
field={bytesField}
|
||||
fieldSearchHighlight={undefined}
|
||||
isEmpty={false}
|
||||
isSelected={false}
|
||||
isActive={false}
|
||||
onClick={undefined}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('renders properly for text-based column field', () => {
|
||||
const component = shallow(
|
||||
<FieldItemButton<DatatableColumn>
|
||||
field={{ id: 'test', name: 'agent', meta: { type: 'string' } }}
|
||||
fieldSearchHighlight="ag"
|
||||
getCustomFieldType={(f) => f.meta.type}
|
||||
isEmpty={false}
|
||||
isSelected={false}
|
||||
isActive={false}
|
||||
onClick={undefined}
|
||||
/>
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -10,7 +10,7 @@ import React from 'react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import classnames from 'classnames';
|
||||
import { FieldButton, type FieldButtonProps } from '@kbn/react-field';
|
||||
import { EuiHighlight } from '@elastic/eui';
|
||||
import { EuiButtonIcon, EuiButtonIconProps, EuiHighlight, EuiIcon, EuiToolTip } from '@elastic/eui';
|
||||
import type { DataViewField } from '@kbn/data-views-plugin/common';
|
||||
import { type FieldListItem, type GetCustomFieldType } from '../../types';
|
||||
import { FieldIcon, getFieldIconProps } from '../field_icon';
|
||||
|
@ -22,72 +22,211 @@ import './field_item_button.scss';
|
|||
export interface FieldItemButtonProps<T extends FieldListItem> {
|
||||
field: T;
|
||||
fieldSearchHighlight?: string;
|
||||
isActive?: FieldButtonProps['isActive'];
|
||||
isEmpty?: boolean; // whether the field has data or not
|
||||
isSelected: boolean; // whether a field is under Selected section
|
||||
isActive: FieldButtonProps['isActive']; // whether a popover is open
|
||||
isEmpty: boolean; // whether the field has data or not
|
||||
infoIcon?: FieldButtonProps['fieldInfoIcon'];
|
||||
className?: FieldButtonProps['className'];
|
||||
flush?: FieldButtonProps['flush'];
|
||||
dragHandle?: FieldButtonProps['dragHandle'];
|
||||
getCustomFieldType?: GetCustomFieldType<T>;
|
||||
dataTestSubj?: string;
|
||||
size?: FieldButtonProps['size'];
|
||||
onClick: FieldButtonProps['onClick'];
|
||||
shouldAlwaysShowAction?: boolean; // should the field action be visible on hover or always
|
||||
buttonAddFieldToWorkspaceProps?: Partial<EuiButtonIconProps>;
|
||||
buttonRemoveFieldFromWorkspaceProps?: Partial<EuiButtonIconProps>;
|
||||
onAddFieldToWorkspace?: (field: T) => unknown;
|
||||
onRemoveFieldFromWorkspace?: (field: T) => unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inner part of field list item
|
||||
* Field list item component
|
||||
* @param field
|
||||
* @param fieldSearchHighlight
|
||||
* @param isSelected
|
||||
* @param isActive
|
||||
* @param isEmpty
|
||||
* @param infoIcon
|
||||
* @param className
|
||||
* @param getCustomFieldType
|
||||
* @param dataTestSubj
|
||||
* @param size
|
||||
* @param onClick
|
||||
* @param shouldAlwaysShowAction
|
||||
* @param buttonAddFieldToWorkspaceProps
|
||||
* @param buttonRemoveFieldFromWorkspaceProps
|
||||
* @param onAddFieldToWorkspace
|
||||
* @param onRemoveFieldFromWorkspace
|
||||
* @param otherProps
|
||||
* @constructor
|
||||
*/
|
||||
export function FieldItemButton<T extends FieldListItem = DataViewField>({
|
||||
field,
|
||||
fieldSearchHighlight,
|
||||
isSelected,
|
||||
isActive,
|
||||
isEmpty,
|
||||
infoIcon,
|
||||
className,
|
||||
getCustomFieldType,
|
||||
dataTestSubj,
|
||||
size,
|
||||
onClick,
|
||||
shouldAlwaysShowAction,
|
||||
buttonAddFieldToWorkspaceProps,
|
||||
buttonRemoveFieldFromWorkspaceProps,
|
||||
onAddFieldToWorkspace,
|
||||
onRemoveFieldFromWorkspace,
|
||||
...otherProps
|
||||
}: FieldItemButtonProps<T>) {
|
||||
const displayName = field.displayName || field.name;
|
||||
const title =
|
||||
displayName !== field.name && field.name !== '___records___'
|
||||
? i18n.translate('unifiedFieldList.fieldItemButton.fieldTitle', {
|
||||
defaultMessage: '{fieldDisplayName} ({fieldName})',
|
||||
values: {
|
||||
fieldName: field.name,
|
||||
fieldDisplayName: displayName,
|
||||
},
|
||||
})
|
||||
: displayName;
|
||||
|
||||
const iconProps = getCustomFieldType
|
||||
? { type: getCustomFieldType(field) }
|
||||
: getFieldIconProps(field);
|
||||
const type = iconProps.type;
|
||||
|
||||
const classes = classnames(
|
||||
'unifiedFieldItemButton',
|
||||
'unifiedFieldListItemButton',
|
||||
{
|
||||
[`unifiedFieldItemButton--${type}`]: type,
|
||||
[`unifiedFieldItemButton--exists`]: !isEmpty,
|
||||
[`unifiedFieldItemButton--missing`]: isEmpty,
|
||||
[`unifiedFieldListItemButton--${type}`]: type,
|
||||
[`unifiedFieldListItemButton--exists`]: !isEmpty,
|
||||
[`unifiedFieldListItemButton--missing`]: isEmpty,
|
||||
},
|
||||
className
|
||||
);
|
||||
|
||||
const addFieldToWorkspaceTooltip =
|
||||
buttonAddFieldToWorkspaceProps?.['aria-label'] ??
|
||||
i18n.translate('unifiedFieldList.fieldItemButton.addFieldToWorkspaceLabel', {
|
||||
defaultMessage: 'Add "{field}" field',
|
||||
values: {
|
||||
field: field.displayName,
|
||||
},
|
||||
});
|
||||
|
||||
const removeFieldFromWorkspaceTooltip =
|
||||
buttonRemoveFieldFromWorkspaceProps?.['aria-label'] ??
|
||||
i18n.translate('unifiedFieldList.fieldItemButton.removeFieldToWorkspaceLabel', {
|
||||
defaultMessage: 'Remove "{field}" field',
|
||||
values: {
|
||||
field: field.displayName,
|
||||
},
|
||||
});
|
||||
|
||||
const fieldActionClassName = classnames('unifiedFieldListItemButton__action', {
|
||||
'unifiedFieldListItemButton__action--always': shouldAlwaysShowAction,
|
||||
});
|
||||
const fieldAction = isSelected
|
||||
? onRemoveFieldFromWorkspace && (
|
||||
<EuiToolTip
|
||||
key={`selected-to-remove-${field.name}-${removeFieldFromWorkspaceTooltip}`}
|
||||
content={removeFieldFromWorkspaceTooltip}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
data-test-subj={`unifiedFieldListItem_removeField-${field.name}`}
|
||||
aria-label={removeFieldFromWorkspaceTooltip}
|
||||
{...(buttonRemoveFieldFromWorkspaceProps || {})}
|
||||
className={classnames(
|
||||
fieldActionClassName,
|
||||
buttonRemoveFieldFromWorkspaceProps?.className
|
||||
)}
|
||||
color="danger"
|
||||
iconType="cross"
|
||||
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onRemoveFieldFromWorkspace(field);
|
||||
}}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
)
|
||||
: onAddFieldToWorkspace && (
|
||||
<EuiToolTip
|
||||
key={`deselected-to-add-${field.name}-${addFieldToWorkspaceTooltip}`}
|
||||
content={addFieldToWorkspaceTooltip}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
data-test-subj={`unifiedFieldListItem_addField-${field.name}`}
|
||||
aria-label={addFieldToWorkspaceTooltip}
|
||||
{...(buttonAddFieldToWorkspaceProps || {})}
|
||||
className={classnames(fieldActionClassName, buttonAddFieldToWorkspaceProps?.className)}
|
||||
color="text"
|
||||
iconType="plusInCircle"
|
||||
onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onAddFieldToWorkspace(field);
|
||||
}}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
|
||||
const conflictInfoIcon = field.type === 'conflict' ? <FieldConflictInfoIcon /> : null;
|
||||
|
||||
return (
|
||||
<FieldButton
|
||||
key={`field-item-button-${field.name}`}
|
||||
dataTestSubj={dataTestSubj}
|
||||
size={size || 's'}
|
||||
className={classes}
|
||||
isActive={isActive}
|
||||
buttonProps={{
|
||||
['aria-label']: i18n.translate('unifiedFieldList.fieldItemButtonAriaLabel', {
|
||||
defaultMessage: 'Preview {fieldName}: {fieldType}',
|
||||
['aria-label']: i18n.translate('unifiedFieldList.fieldItemButton.ariaLabel', {
|
||||
defaultMessage: 'Preview {fieldDisplayName}: {fieldType}',
|
||||
values: {
|
||||
fieldName: displayName,
|
||||
fieldDisplayName: displayName,
|
||||
fieldType: getCustomFieldType ? getCustomFieldType(field) : field.type,
|
||||
},
|
||||
}),
|
||||
}}
|
||||
fieldIcon={<FieldIcon {...iconProps} />}
|
||||
fieldName={<EuiHighlight search={fieldSearchHighlight || ''}>{displayName}</EuiHighlight>}
|
||||
fieldInfoIcon={infoIcon}
|
||||
fieldName={
|
||||
<EuiHighlight
|
||||
search={fieldSearchHighlight || ''}
|
||||
title={title}
|
||||
data-test-subj={`field-${field.name}`}
|
||||
>
|
||||
{displayName}
|
||||
</EuiHighlight>
|
||||
}
|
||||
fieldAction={fieldAction}
|
||||
fieldInfoIcon={conflictInfoIcon || infoIcon}
|
||||
onClick={onClick}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function FieldConflictInfoIcon() {
|
||||
return (
|
||||
<EuiToolTip
|
||||
position="bottom"
|
||||
content={i18n.translate('unifiedFieldList.fieldItemButton.mappingConflictDescription', {
|
||||
defaultMessage:
|
||||
'This field is defined as several types (string, integer, etc) across the indices that match this pattern.' +
|
||||
'You may still be able to use this conflicting field, but it will be unavailable for functions that require Kibana to know their type. Correcting this issue will require reindexing your data.',
|
||||
})}
|
||||
>
|
||||
<EuiIcon
|
||||
tabIndex={0}
|
||||
type="warning"
|
||||
title={i18n.translate('unifiedFieldList.fieldItemButton.mappingConflictTitle', {
|
||||
defaultMessage: 'Mapping Conflict',
|
||||
})}
|
||||
size="s"
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -60,6 +60,7 @@ export type {
|
|||
FieldListGroups,
|
||||
FieldsGroupDetails,
|
||||
FieldTypeKnown,
|
||||
FieldListItem,
|
||||
GetCustomFieldType,
|
||||
} from './types';
|
||||
export { ExistenceFetchStatus, FieldsGroupNames } from './types';
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
"@kbn/core-lifecycle-browser",
|
||||
"@kbn/react-field",
|
||||
"@kbn/field-types",
|
||||
"@kbn/expressions-plugin",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
71
test/functional/apps/discover/group3/_drag_drop.ts
Normal file
71
test/functional/apps/discover/group3/_drag_drop.ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const PageObjects = getPageObjects(['common', 'discover', 'timePicker', 'header']);
|
||||
|
||||
describe('discover drag and drop', function describeIndexTests() {
|
||||
before(async function () {
|
||||
await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover');
|
||||
await kibanaServer.uiSettings.replace({
|
||||
defaultIndex: 'logstash-*',
|
||||
});
|
||||
await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings();
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover');
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
await kibanaServer.uiSettings.replace({});
|
||||
await PageObjects.discover.cleanSidebarLocalStorage();
|
||||
});
|
||||
|
||||
describe('should add fields as columns via drag and drop', function () {
|
||||
it('should support dragging and dropping a field onto the grid', async function () {
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.discover.waitUntilSidebarHasLoaded();
|
||||
|
||||
expect(await PageObjects.discover.getSidebarAriaDescription()).to.be(
|
||||
'53 available fields. 0 empty fields. 3 meta fields.'
|
||||
);
|
||||
expect((await PageObjects.discover.getColumnHeaders()).join(', ')).to.be(
|
||||
'@timestamp, Document'
|
||||
);
|
||||
|
||||
await PageObjects.discover.dragFieldToTable('extension');
|
||||
|
||||
expect((await PageObjects.discover.getColumnHeaders()).join(', ')).to.be(
|
||||
'@timestamp, extension'
|
||||
);
|
||||
|
||||
await PageObjects.discover.dragFieldWithKeyboardToTable('@message');
|
||||
|
||||
expect((await PageObjects.discover.getColumnHeaders()).join(', ')).to.be(
|
||||
'@timestamp, extension, @message'
|
||||
);
|
||||
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
|
||||
expect(
|
||||
(await PageObjects.discover.getSidebarSectionFieldNames('selected')).join(', ')
|
||||
).to.be('extension, @message');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -20,6 +20,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
|
|||
await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional');
|
||||
});
|
||||
|
||||
loadTestFile(require.resolve('./_drag_drop'));
|
||||
loadTestFile(require.resolve('./_sidebar'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -394,7 +394,7 @@ export class DiscoverPageObject extends FtrService {
|
|||
public async getAllFieldNames() {
|
||||
const sidebar = await this.testSubjects.find('discover-sidebar');
|
||||
const $ = await sidebar.parseDomContent();
|
||||
return $('.dscSidebarField__name')
|
||||
return $('.kbnFieldButton__name')
|
||||
.toArray()
|
||||
.map((field) => $(field).text());
|
||||
}
|
||||
|
@ -868,4 +868,51 @@ export class DiscoverPageObject extends FtrService {
|
|||
await this.fieldEditor.save();
|
||||
await this.header.waitUntilLoadingHasFinished();
|
||||
}
|
||||
|
||||
private async waitForDropToFinish() {
|
||||
await this.retry.try(async () => {
|
||||
const exists = await this.find.existsByCssSelector('.domDragDrop-isActiveGroup');
|
||||
if (exists) {
|
||||
throw new Error('UI still in drag/drop mode');
|
||||
}
|
||||
});
|
||||
await this.header.waitUntilLoadingHasFinished();
|
||||
await this.waitUntilSearchingHasFinished();
|
||||
}
|
||||
|
||||
/**
|
||||
* Drags field to add as a column
|
||||
*
|
||||
* @param fieldName
|
||||
* */
|
||||
public async dragFieldToTable(fieldName: string) {
|
||||
await this.waitUntilSidebarHasLoaded();
|
||||
|
||||
const from = `dscFieldListPanelField-${fieldName}`;
|
||||
await this.find.existsByCssSelector(from);
|
||||
await this.browser.html5DragAndDrop(
|
||||
this.testSubjects.getCssSelector(from),
|
||||
this.testSubjects.getCssSelector('dscMainContent')
|
||||
);
|
||||
await this.waitForDropToFinish();
|
||||
}
|
||||
|
||||
/**
|
||||
* Drags field with keyboard actions to add as a column
|
||||
*
|
||||
* @param fieldName
|
||||
* */
|
||||
public async dragFieldWithKeyboardToTable(fieldName: string) {
|
||||
const field = await this.find.byCssSelector(
|
||||
`[data-test-subj="domDragDrop_draggable-${fieldName}"] [data-test-subj="domDragDrop-keyboardHandler"]`
|
||||
);
|
||||
await field.focus();
|
||||
await this.retry.try(async () => {
|
||||
await this.browser.pressKeys(this.browser.keys.ENTER);
|
||||
await this.testSubjects.exists('.domDragDrop-isDropTarget'); // checks if we're in dnd mode and there's any drop target active
|
||||
});
|
||||
await this.browser.pressKeys(this.browser.keys.RIGHT);
|
||||
await this.browser.pressKeys(this.browser.keys.ENTER);
|
||||
await this.waitForDropToFinish();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
.lnsFieldItem__fieldPanel {
|
||||
min-width: 260px;
|
||||
max-width: 300px;
|
||||
}
|
|
@ -9,16 +9,15 @@ import React, { ReactElement } from 'react';
|
|||
import { ReactWrapper } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { EuiLoadingSpinner, EuiPopover } from '@elastic/eui';
|
||||
import { InnerFieldItem, FieldItemProps } from './field_item';
|
||||
import { InnerFieldItem, FieldItemIndexPatternFieldProps } from './field_item';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
import { fieldFormatsServiceMock } from '@kbn/field-formats-plugin/public/mocks';
|
||||
import { IndexPattern } from '../../types';
|
||||
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
|
||||
import { documentField } from './document_field';
|
||||
import { documentField } from '../form_based/document_field';
|
||||
import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
|
||||
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
||||
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
|
||||
|
@ -32,12 +31,10 @@ jest.mock('@kbn/unified-field-list-plugin/public/services/field_stats', () => ({
|
|||
loadFieldStats: jest.fn().mockResolvedValue({}),
|
||||
}));
|
||||
|
||||
const chartsThemeService = chartPluginMock.createSetupContract().theme;
|
||||
|
||||
const clickField = async (wrapper: ReactWrapper, field: string) => {
|
||||
await act(async () => {
|
||||
await wrapper
|
||||
.find(`[data-test-subj="lnsFieldListPanelField-${field}"] button`)
|
||||
.find(`[data-test-subj="lnsFieldListPanelField-${field}"] .kbnFieldButton__button`)
|
||||
.simulate('click');
|
||||
});
|
||||
};
|
||||
|
@ -47,6 +44,7 @@ const mockedServices = {
|
|||
dataViews: dataViewPluginMocks.createStartContract(),
|
||||
fieldFormats: fieldFormatsServiceMock.createStartContract(),
|
||||
charts: chartPluginMock.createSetupContract(),
|
||||
uiActions: uiActionsPluginMock.createStartContract(),
|
||||
uiSettings: coreMock.createStart().uiSettings,
|
||||
share: {
|
||||
url: {
|
||||
|
@ -64,7 +62,7 @@ const mockedServices = {
|
|||
},
|
||||
};
|
||||
|
||||
const InnerFieldItemWrapper: React.FC<FieldItemProps> = (props) => {
|
||||
const InnerFieldItemWrapper: React.FC<FieldItemIndexPatternFieldProps> = (props) => {
|
||||
return (
|
||||
<KibanaContextProvider services={mockedServices}>
|
||||
<InnerFieldItem {...props} />
|
||||
|
@ -72,7 +70,7 @@ const InnerFieldItemWrapper: React.FC<FieldItemProps> = (props) => {
|
|||
);
|
||||
};
|
||||
|
||||
async function getComponent(props: FieldItemProps) {
|
||||
async function getComponent(props: FieldItemIndexPatternFieldProps) {
|
||||
const instance = await mountWithIntl(<InnerFieldItemWrapper {...props} />);
|
||||
// wait for lazy modules
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
@ -80,8 +78,8 @@ async function getComponent(props: FieldItemProps) {
|
|||
return instance;
|
||||
}
|
||||
|
||||
describe('IndexPattern Field Item', () => {
|
||||
let defaultProps: FieldItemProps;
|
||||
describe('Lens Field Item', () => {
|
||||
let defaultProps: FieldItemIndexPatternFieldProps;
|
||||
let indexPattern: IndexPattern;
|
||||
let dataView: DataView;
|
||||
|
||||
|
@ -146,13 +144,6 @@ describe('IndexPattern Field Item', () => {
|
|||
|
||||
defaultProps = {
|
||||
indexPattern,
|
||||
fieldFormats: {
|
||||
...fieldFormatsServiceMock.createStartContract(),
|
||||
getDefaultInstance: jest.fn(() => ({
|
||||
convert: jest.fn((s: unknown) => JSON.stringify(s)),
|
||||
})),
|
||||
} as unknown as FieldFormatsStart,
|
||||
core: coreMock.createStart(),
|
||||
highlight: '',
|
||||
dateRange: {
|
||||
fromDate: 'now-7d',
|
||||
|
@ -168,17 +159,17 @@ describe('IndexPattern Field Item', () => {
|
|||
searchable: true,
|
||||
},
|
||||
exists: true,
|
||||
chartsThemeService,
|
||||
groupIndex: 0,
|
||||
itemIndex: 0,
|
||||
dropOntoWorkspace: () => {},
|
||||
hasSuggestionForField: () => false,
|
||||
uiActions: uiActionsPluginMock.createStartContract(),
|
||||
};
|
||||
|
||||
dataView = {
|
||||
...indexPattern,
|
||||
getFormatterForField: defaultProps.fieldFormats.getDefaultInstance,
|
||||
getFormatterForField: jest.fn(() => ({
|
||||
convert: jest.fn((s: unknown) => JSON.stringify(s)),
|
||||
})),
|
||||
} as unknown as DataView;
|
||||
|
||||
(mockedServices.dataViews.get as jest.Mock).mockImplementation(() => {
|
|
@ -8,14 +8,11 @@
|
|||
import './field_item.scss';
|
||||
|
||||
import React, { useCallback, useState, useMemo } from 'react';
|
||||
import { EuiText, EuiButton, EuiPopoverFooter, EuiIconTip } from '@elastic/eui';
|
||||
import { EuiText, EuiButton, EuiPopoverFooter } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
import { Filter, Query } from '@kbn/es-query';
|
||||
import { DataViewField, type DataView } from '@kbn/data-views-plugin/common';
|
||||
import { ChartsPluginSetup } from '@kbn/charts-plugin/public';
|
||||
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
import {
|
||||
AddFieldFilterHandler,
|
||||
FieldStats,
|
||||
|
@ -23,37 +20,59 @@ import {
|
|||
FieldPopoverHeader,
|
||||
FieldPopoverVisualize,
|
||||
FieldItemButton,
|
||||
type GetCustomFieldType,
|
||||
} from '@kbn/unified-field-list-plugin/public';
|
||||
import { DragDrop } from '@kbn/dom-drag-drop';
|
||||
import { generateFilters, getEsQueryConfig } from '@kbn/data-plugin/public';
|
||||
import { type DatatableColumn } from '@kbn/expressions-plugin/common';
|
||||
import { DatasourceDataPanelProps } from '../../types';
|
||||
import type { IndexPattern, IndexPatternField } from '../../types';
|
||||
import type { LensAppServices } from '../../app_plugin/types';
|
||||
import { APP_ID, DOCUMENT_FIELD_NAME } from '../../../common/constants';
|
||||
import { combineQueryAndFilters } from '../../app_plugin/show_underlying_data';
|
||||
import { getFieldItemActions } from './get_field_item_actions';
|
||||
|
||||
export interface FieldItemProps {
|
||||
core: DatasourceDataPanelProps['core'];
|
||||
fieldFormats: FieldFormatsStart;
|
||||
field: IndexPatternField;
|
||||
indexPattern: IndexPattern;
|
||||
type LensFieldListItem = IndexPatternField | DatatableColumn | DataViewField;
|
||||
|
||||
function isTextBasedColumnField(field: LensFieldListItem): field is DatatableColumn {
|
||||
return !('type' in field) && Boolean(field?.meta.type);
|
||||
}
|
||||
|
||||
interface FieldItemBaseProps {
|
||||
highlight?: string;
|
||||
exists: boolean;
|
||||
query: Query;
|
||||
dateRange: DatasourceDataPanelProps['dateRange'];
|
||||
chartsThemeService: ChartsPluginSetup['theme'];
|
||||
filters: Filter[];
|
||||
hideDetails?: boolean;
|
||||
itemIndex: number;
|
||||
groupIndex: number;
|
||||
dropOntoWorkspace: DatasourceDataPanelProps['dropOntoWorkspace'];
|
||||
editField?: (name: string) => void;
|
||||
removeField?: (name: string) => void;
|
||||
hasSuggestionForField: DatasourceDataPanelProps['hasSuggestionForField'];
|
||||
uiActions: UiActionsStart;
|
||||
}
|
||||
|
||||
export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) {
|
||||
export interface FieldItemIndexPatternFieldProps extends FieldItemBaseProps {
|
||||
field: IndexPatternField;
|
||||
indexPattern: IndexPattern;
|
||||
query: Query;
|
||||
dateRange: DatasourceDataPanelProps['dateRange'];
|
||||
filters: Filter[];
|
||||
editField?: (name: string) => void;
|
||||
removeField?: (name: string) => void;
|
||||
getCustomFieldType?: never;
|
||||
}
|
||||
|
||||
export interface FieldItemDatatableColumnProps extends FieldItemBaseProps {
|
||||
field: DatatableColumn;
|
||||
indexPattern?: never;
|
||||
query?: never;
|
||||
dateRange?: never;
|
||||
filters?: never;
|
||||
editField?: never;
|
||||
removeField?: never;
|
||||
getCustomFieldType: GetCustomFieldType<DatatableColumn>;
|
||||
}
|
||||
|
||||
export type FieldItemProps = FieldItemIndexPatternFieldProps | FieldItemDatatableColumnProps;
|
||||
|
||||
export function InnerFieldItem(props: FieldItemProps) {
|
||||
const {
|
||||
field,
|
||||
indexPattern,
|
||||
|
@ -66,9 +85,21 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) {
|
|||
hasSuggestionForField,
|
||||
editField,
|
||||
removeField,
|
||||
getCustomFieldType,
|
||||
} = props;
|
||||
|
||||
const dataViewField = useMemo(() => new DataViewField(field), [field]);
|
||||
const dataViewField = useMemo(() => {
|
||||
// DatatableColumn type
|
||||
if (isTextBasedColumnField(field)) {
|
||||
return new DataViewField({
|
||||
name: field.name,
|
||||
type: field.meta?.type ?? 'unknown',
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
});
|
||||
}
|
||||
// IndexPatternField type
|
||||
return new DataViewField(field);
|
||||
}, [field]);
|
||||
const services = useKibana<LensAppServices>().services;
|
||||
const filterManager = services?.data?.query?.filterManager;
|
||||
const [infoIsOpen, setOpen] = useState(false);
|
||||
|
@ -83,7 +114,7 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) {
|
|||
|
||||
const addFilterAndClose: AddFieldFilterHandler | undefined = useMemo(
|
||||
() =>
|
||||
filterManager
|
||||
filterManager && indexPattern
|
||||
? (clickedField, values, operation) => {
|
||||
closePopover();
|
||||
const newFilters = generateFilters(
|
||||
|
@ -121,52 +152,45 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) {
|
|||
[removeField, closePopover]
|
||||
);
|
||||
|
||||
const indexPatternId = indexPattern?.id;
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
field,
|
||||
indexPatternId: indexPattern.id,
|
||||
id: field.name,
|
||||
humanData: {
|
||||
label: field.displayName,
|
||||
position: itemIndex + 1,
|
||||
},
|
||||
}),
|
||||
[field, indexPattern.id, itemIndex]
|
||||
() =>
|
||||
isTextBasedColumnField(field)
|
||||
? {
|
||||
field: field.name,
|
||||
id: field.id,
|
||||
humanData: { label: field.name },
|
||||
}
|
||||
: {
|
||||
field,
|
||||
indexPatternId,
|
||||
id: field.name,
|
||||
humanData: {
|
||||
label: field.displayName,
|
||||
position: itemIndex + 1,
|
||||
},
|
||||
},
|
||||
[field, indexPatternId, itemIndex]
|
||||
);
|
||||
|
||||
const dropOntoWorkspaceAndClose = useCallback(() => {
|
||||
closePopover();
|
||||
dropOntoWorkspace(value);
|
||||
}, [dropOntoWorkspace, closePopover, value]);
|
||||
|
||||
const onDragStart = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, [setOpen]);
|
||||
|
||||
const order = useMemo(() => [0, groupIndex, itemIndex], [groupIndex, itemIndex]);
|
||||
|
||||
const lensInfoIcon = (
|
||||
<EuiIconTip
|
||||
anchorClassName="lnsFieldItem__infoIcon"
|
||||
content={
|
||||
hideDetails
|
||||
? i18n.translate('xpack.lens.indexPattern.fieldItemTooltip', {
|
||||
defaultMessage: 'Drag and drop to visualize.',
|
||||
})
|
||||
: exists
|
||||
? i18n.translate('xpack.lens.indexPattern.fieldStatsButtonLabel', {
|
||||
defaultMessage: 'Click for a field preview, or drag and drop to visualize.',
|
||||
})
|
||||
: i18n.translate('xpack.lens.indexPattern.fieldStatsButtonEmptyLabel', {
|
||||
defaultMessage:
|
||||
'This field doesn’t have any data but you can still drag and drop to visualize.',
|
||||
})
|
||||
}
|
||||
type="iInCircle"
|
||||
color="subdued"
|
||||
size="s"
|
||||
/>
|
||||
);
|
||||
const { buttonAddFieldToWorkspaceProps, onAddFieldToWorkspace } = getFieldItemActions({
|
||||
value,
|
||||
hasSuggestionForField,
|
||||
dropOntoWorkspace,
|
||||
closeFieldPopover: closePopover,
|
||||
});
|
||||
|
||||
const commonFieldItemButtonProps = {
|
||||
isSelected: false, // multiple selections are allowed
|
||||
isEmpty: !exists,
|
||||
isActive: infoIsOpen,
|
||||
fieldSearchHighlight: highlight,
|
||||
onClick: togglePopover,
|
||||
buttonAddFieldToWorkspaceProps,
|
||||
onAddFieldToWorkspace,
|
||||
};
|
||||
|
||||
return (
|
||||
<li>
|
||||
|
@ -187,41 +211,26 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) {
|
|||
order={order}
|
||||
value={value}
|
||||
dataTestSubj={`lnsFieldListPanelField-${field.name}`}
|
||||
onDragStart={onDragStart}
|
||||
onDragStart={closePopover}
|
||||
>
|
||||
<FieldItemButton<IndexPatternField>
|
||||
isEmpty={!exists}
|
||||
isActive={infoIsOpen}
|
||||
infoIcon={lensInfoIcon}
|
||||
field={field}
|
||||
fieldSearchHighlight={highlight}
|
||||
onClick={togglePopover}
|
||||
/>
|
||||
{isTextBasedColumnField(field) ? (
|
||||
<FieldItemButton<DatatableColumn>
|
||||
field={field}
|
||||
getCustomFieldType={getCustomFieldType}
|
||||
{...commonFieldItemButtonProps}
|
||||
/>
|
||||
) : (
|
||||
<FieldItemButton field={field} {...commonFieldItemButtonProps} />
|
||||
)}
|
||||
</DragDrop>
|
||||
}
|
||||
renderHeader={() => {
|
||||
const canAddToWorkspace = hasSuggestionForField(value);
|
||||
const buttonTitle = canAddToWorkspace
|
||||
? i18n.translate('xpack.lens.indexPattern.moveToWorkspace', {
|
||||
defaultMessage: 'Add {field} to workspace',
|
||||
values: {
|
||||
field: value.field.name,
|
||||
},
|
||||
})
|
||||
: i18n.translate('xpack.lens.indexPattern.moveToWorkspaceNotAvailable', {
|
||||
defaultMessage:
|
||||
'To visualize this field, please add it directly to the desired layer. Adding this field to the workspace is not supported based on your current configuration.',
|
||||
});
|
||||
|
||||
return (
|
||||
<FieldPopoverHeader
|
||||
field={dataViewField}
|
||||
closePopover={closePopover}
|
||||
buttonAddFieldToWorkspaceProps={{
|
||||
isDisabled: !canAddToWorkspace,
|
||||
'aria-label': buttonTitle,
|
||||
}}
|
||||
onAddFieldToWorkspace={dropOntoWorkspaceAndClose}
|
||||
buttonAddFieldToWorkspaceProps={buttonAddFieldToWorkspaceProps}
|
||||
onAddFieldToWorkspace={onAddFieldToWorkspace}
|
||||
onAddFilter={addFilterAndClose}
|
||||
onEditField={editFieldAndClose}
|
||||
onDeleteField={removeFieldAndClose}
|
||||
|
@ -242,9 +251,9 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) {
|
|||
/>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const FieldItem = React.memo(InnerFieldItem);
|
||||
export const FieldItem = React.memo(InnerFieldItem) as typeof InnerFieldItem;
|
||||
|
||||
function FieldItemPopoverContents(
|
||||
props: FieldItemProps & {
|
||||
|
@ -252,10 +261,13 @@ function FieldItemPopoverContents(
|
|||
onAddFilter: AddFieldFilterHandler | undefined;
|
||||
}
|
||||
) {
|
||||
const { query, filters, indexPattern, dataViewField, dateRange, onAddFilter, uiActions } = props;
|
||||
const { query, filters, indexPattern, dataViewField, dateRange, onAddFilter } = props;
|
||||
const services = useKibana<LensAppServices>().services;
|
||||
|
||||
const exploreInDiscover = useMemo(() => {
|
||||
if (!indexPattern) {
|
||||
return null;
|
||||
}
|
||||
const meta = {
|
||||
id: indexPattern.id,
|
||||
columns: [dataViewField.name],
|
||||
|
@ -290,6 +302,10 @@ function FieldItemPopoverContents(
|
|||
});
|
||||
}, [dataViewField.name, filters, indexPattern, query, services]);
|
||||
|
||||
if (!indexPattern) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FieldStats
|
||||
|
@ -330,7 +346,7 @@ function FieldItemPopoverContents(
|
|||
field={dataViewField}
|
||||
dataView={{ ...indexPattern, toSpec: () => indexPattern.spec } as unknown as DataView}
|
||||
originatingApp={APP_ID}
|
||||
uiActions={uiActions}
|
||||
uiActions={services.uiActions}
|
||||
buttonProps={{
|
||||
'data-test-subj': `lensVisualize-GeoField-${dataViewField.name}`,
|
||||
}}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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 { type DragDropIdentifier } from '@kbn/dom-drag-drop';
|
||||
import type { FieldItemButtonProps, FieldListItem } from '@kbn/unified-field-list-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { type DataViewField } from '@kbn/data-views-plugin/common';
|
||||
|
||||
interface GetFieldItemActionsParams<T extends FieldListItem> {
|
||||
value: DragDropIdentifier;
|
||||
dropOntoWorkspace: (value: DragDropIdentifier) => void;
|
||||
hasSuggestionForField: (value: DragDropIdentifier) => boolean;
|
||||
closeFieldPopover?: () => void;
|
||||
}
|
||||
|
||||
interface GetFieldItemActionsResult<T extends FieldListItem> {
|
||||
buttonAddFieldToWorkspaceProps: FieldItemButtonProps<T>['buttonAddFieldToWorkspaceProps'];
|
||||
onAddFieldToWorkspace: FieldItemButtonProps<T | DataViewField>['onAddFieldToWorkspace'];
|
||||
}
|
||||
|
||||
export function getFieldItemActions<T extends FieldListItem>({
|
||||
value,
|
||||
hasSuggestionForField,
|
||||
dropOntoWorkspace,
|
||||
closeFieldPopover,
|
||||
}: GetFieldItemActionsParams<T>): GetFieldItemActionsResult<T> {
|
||||
const canAddToWorkspace = hasSuggestionForField(value);
|
||||
const addToWorkplaceButtonTitle = canAddToWorkspace
|
||||
? i18n.translate('xpack.lens.indexPattern.moveToWorkspace', {
|
||||
defaultMessage: 'Add {field} to workspace',
|
||||
values: {
|
||||
field: value.humanData.label,
|
||||
},
|
||||
})
|
||||
: i18n.translate('xpack.lens.indexPattern.moveToWorkspaceNotAvailable', {
|
||||
defaultMessage:
|
||||
'To visualize this field, please add it directly to the desired layer. Adding this field to the workspace is not supported based on your current configuration.',
|
||||
});
|
||||
|
||||
const dropOntoWorkspaceAndClose = () => {
|
||||
closeFieldPopover?.();
|
||||
dropOntoWorkspace(value);
|
||||
};
|
||||
|
||||
return {
|
||||
buttonAddFieldToWorkspaceProps: {
|
||||
isDisabled: !canAddToWorkspace,
|
||||
'aria-label': addToWorkplaceButtonTitle,
|
||||
},
|
||||
onAddFieldToWorkspace: dropOntoWorkspaceAndClose,
|
||||
};
|
||||
}
|
|
@ -17,7 +17,7 @@ import { InnerFormBasedDataPanel, FormBasedDataPanel } from './datapanel';
|
|||
import { FieldListGrouped } from '@kbn/unified-field-list-plugin/public';
|
||||
import * as UseExistingFieldsApi from '@kbn/unified-field-list-plugin/public/hooks/use_existing_fields';
|
||||
import * as ExistingFieldsServiceApi from '@kbn/unified-field-list-plugin/public/services/field_existing/load_field_existing';
|
||||
import { FieldItem } from './field_item';
|
||||
import { FieldItem } from '../common/field_item';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { FormBasedPrivateState } from './types';
|
||||
|
@ -37,6 +37,7 @@ import { DataViewsState } from '../../state_management';
|
|||
import { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { UI_SETTINGS } from '@kbn/data-plugin/public';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import { IndexPatternField } from '../../types';
|
||||
|
||||
const fieldsOne = [
|
||||
{
|
||||
|
@ -787,7 +788,11 @@ describe('FormBased Data Panel', () => {
|
|||
it('should list all supported fields in the pattern sorted alphabetically in groups', async () => {
|
||||
const wrapper = await mountAndWaitForLazyModules(<InnerFormBasedDataPanel {...props} />);
|
||||
|
||||
expect(wrapper.find(FieldItem).first().prop('field').displayName).toEqual('Records');
|
||||
expect(wrapper.find(FieldItem).first().prop('field')).toEqual(
|
||||
expect.objectContaining({
|
||||
displayName: 'Records',
|
||||
})
|
||||
);
|
||||
const availableAccordion = wrapper.find('[data-test-subj="lnsIndexPatternAvailableFields"]');
|
||||
expect(
|
||||
availableAccordion.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name)
|
||||
|
@ -803,7 +808,9 @@ describe('FormBased Data Panel', () => {
|
|||
emptyAccordion.find(FieldItem).map((fieldItem) => fieldItem.prop('field').name)
|
||||
).toEqual(['client', 'source', 'timestamp']);
|
||||
expect(
|
||||
emptyAccordion.find(FieldItem).map((fieldItem) => fieldItem.prop('field').displayName)
|
||||
emptyAccordion
|
||||
.find(FieldItem)
|
||||
.map((fieldItem) => (fieldItem.prop('field') as IndexPatternField).displayName)
|
||||
).toEqual(['client', 'source', 'timestampLabel']);
|
||||
expect(emptyAccordion.find(FieldItem).at(1).prop('exists')).toEqual(false);
|
||||
});
|
||||
|
@ -872,7 +879,7 @@ describe('FormBased Data Panel', () => {
|
|||
wrapper
|
||||
.find('[data-test-subj="lnsIndexPatternEmptyFields"]')
|
||||
.find(FieldItem)
|
||||
.map((fieldItem) => fieldItem.prop('field').displayName)
|
||||
.map((fieldItem) => (fieldItem.prop('field') as IndexPatternField).displayName)
|
||||
).toEqual(['amemory', 'bytes', 'client', 'source', 'timestampLabel']);
|
||||
});
|
||||
|
||||
|
@ -974,7 +981,9 @@ describe('FormBased Data Panel', () => {
|
|||
wrapper.find('[data-test-subj="typeFilter-number"]').first().simulate('click');
|
||||
|
||||
expect(
|
||||
wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').displayName)
|
||||
wrapper
|
||||
.find(FieldItem)
|
||||
.map((fieldItem) => (fieldItem.prop('field') as IndexPatternField).displayName)
|
||||
).toEqual(['amemory', 'bytes']);
|
||||
});
|
||||
|
||||
|
@ -1010,7 +1019,9 @@ describe('FormBased Data Panel', () => {
|
|||
.first()
|
||||
.simulate('click');
|
||||
expect(
|
||||
wrapper.find(FieldItem).map((fieldItem) => fieldItem.prop('field').displayName)
|
||||
wrapper
|
||||
.find(FieldItem)
|
||||
.map((fieldItem) => (fieldItem.prop('field') as IndexPatternField).displayName)
|
||||
).toEqual(['Records', 'amemory', 'bytes', 'client', 'source', 'timestampLabel']);
|
||||
});
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ import type {
|
|||
} from '../../types';
|
||||
import type { FormBasedPrivateState } from './types';
|
||||
import { IndexPatternServiceAPI } from '../../data_views_service/service';
|
||||
import { FieldItem } from './field_item';
|
||||
import { FieldItem } from '../common/field_item';
|
||||
|
||||
export type Props = Omit<
|
||||
DatasourceDataPanelProps<FormBasedPrivateState>,
|
||||
|
@ -175,9 +175,7 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({
|
|||
core,
|
||||
data,
|
||||
dataViews,
|
||||
fieldFormats,
|
||||
indexPatternFieldEditor,
|
||||
charts,
|
||||
dropOntoWorkspace,
|
||||
hasSuggestionForField,
|
||||
uiActions,
|
||||
|
@ -380,30 +378,22 @@ export const InnerFormBasedDataPanel = function InnerFormBasedDataPanel({
|
|||
hasSuggestionForField={hasSuggestionForField}
|
||||
editField={editField}
|
||||
removeField={removeField}
|
||||
uiActions={uiActions}
|
||||
core={core}
|
||||
fieldFormats={fieldFormats}
|
||||
indexPattern={currentIndexPattern}
|
||||
highlight={fieldSearchHighlight}
|
||||
dateRange={dateRange}
|
||||
query={query}
|
||||
filters={filters}
|
||||
chartsThemeService={charts.theme}
|
||||
/>
|
||||
),
|
||||
[
|
||||
core,
|
||||
fieldFormats,
|
||||
currentIndexPattern,
|
||||
dateRange,
|
||||
query,
|
||||
filters,
|
||||
charts.theme,
|
||||
dropOntoWorkspace,
|
||||
hasSuggestionForField,
|
||||
editField,
|
||||
removeField,
|
||||
uiActions,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
.kbnFieldButton {
|
||||
.lnsFieldItem__infoIcon {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover:not([class*='isActive']) {
|
||||
.lnsFieldItem__infoIcon {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transition: opacity $euiAnimSpeedFast ease-in-out 1s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.kbnFieldButton.domDragDrop_ghost {
|
||||
.lnsFieldItem__infoIcon {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.lnsFieldItem__fieldPanel {
|
||||
min-width: 260px;
|
||||
max-width: 300px;
|
||||
}
|
|
@ -18,7 +18,7 @@ import {
|
|||
Start as DataViewPublicStart,
|
||||
} from '@kbn/data-views-plugin/public/mocks';
|
||||
import type { DatatableColumn } from '@kbn/expressions-plugin/public';
|
||||
import { EuiHighlight } from '@elastic/eui';
|
||||
import { EuiHighlight, EuiToken } from '@elastic/eui';
|
||||
|
||||
import { type TextBasedDataPanelProps, TextBasedDataPanel } from './datapanel';
|
||||
|
||||
|
@ -274,4 +274,15 @@ describe('TextBased Query Languages Data Panel', () => {
|
|||
.map((item) => item.prop('children'))
|
||||
).toEqual(['memory']);
|
||||
});
|
||||
|
||||
it('should render correct field type icons', async () => {
|
||||
const wrapper = await mountAndWaitForLazyModules(<TextBasedDataPanel {...defaultProps} />);
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test-subj="lnsTextBasedLanguagesAvailableFields"]')
|
||||
.find(EuiToken)
|
||||
.map((item) => item.prop('iconType'))
|
||||
).toEqual(['tokenNumber', 'tokenNumber', 'tokenDate']);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,17 +18,17 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
|||
import {
|
||||
FieldList,
|
||||
FieldListFilters,
|
||||
FieldItemButton,
|
||||
GetCustomFieldType,
|
||||
FieldListGrouped,
|
||||
FieldListGroupedProps,
|
||||
FieldsGroupNames,
|
||||
GetCustomFieldType,
|
||||
useGroupedFields,
|
||||
} from '@kbn/unified-field-list-plugin/public';
|
||||
import { ChildDragDropProvider, DragDrop } from '@kbn/dom-drag-drop';
|
||||
import { ChildDragDropProvider } from '@kbn/dom-drag-drop';
|
||||
import type { DatasourceDataPanelProps } from '../../types';
|
||||
import type { TextBasedPrivateState } from './types';
|
||||
import { getStateFromAggregateQuery } from './utils';
|
||||
import { FieldItem } from '../common/field_item';
|
||||
|
||||
const getCustomFieldType: GetCustomFieldType<DatatableColumn> = (field) => field?.meta.type;
|
||||
|
||||
|
@ -52,6 +52,8 @@ export function TextBasedDataPanel({
|
|||
expressions,
|
||||
dataViews,
|
||||
layerFields,
|
||||
hasSuggestionForField,
|
||||
dropOntoWorkspace,
|
||||
}: TextBasedDataPanelProps) {
|
||||
const prevQuery = usePrevious(query);
|
||||
const [dataHasLoaded, setDataHasLoaded] = useState(false);
|
||||
|
@ -108,33 +110,26 @@ export function TextBasedDataPanel({
|
|||
});
|
||||
|
||||
const renderFieldItem: FieldListGroupedProps<DatatableColumn>['renderFieldItem'] = useCallback(
|
||||
({ field, itemIndex, fieldSearchHighlight }) => {
|
||||
({ field, groupIndex, itemIndex, fieldSearchHighlight, groupName }) => {
|
||||
if (!field) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<DragDrop
|
||||
draggable
|
||||
order={[itemIndex]}
|
||||
value={{
|
||||
field: field.name,
|
||||
id: field.id,
|
||||
humanData: { label: field.name },
|
||||
}}
|
||||
dataTestSubj={`lnsFieldListPanelField-${field.name}`}
|
||||
>
|
||||
<FieldItemButton<DatatableColumn>
|
||||
isEmpty={false}
|
||||
isActive={false}
|
||||
field={field}
|
||||
fieldSearchHighlight={fieldSearchHighlight}
|
||||
getCustomFieldType={getCustomFieldType}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
</DragDrop>
|
||||
<FieldItem
|
||||
field={field}
|
||||
exists
|
||||
hideDetails
|
||||
itemIndex={itemIndex}
|
||||
groupIndex={groupIndex}
|
||||
dropOntoWorkspace={dropOntoWorkspace}
|
||||
hasSuggestionForField={hasSuggestionForField}
|
||||
highlight={fieldSearchHighlight}
|
||||
getCustomFieldType={getCustomFieldType}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[]
|
||||
[hasSuggestionForField, dropOntoWorkspace]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -13,7 +13,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
|
||||
import type { AggregateQuery } from '@kbn/es-query';
|
||||
import type { SavedObjectReference } from '@kbn/core/public';
|
||||
import { EuiButtonEmpty, EuiFormRow } from '@elastic/eui';
|
||||
import { EuiFormRow } from '@elastic/eui';
|
||||
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import type { ExpressionsStart, DatatableColumnType } from '@kbn/expressions-plugin/public';
|
||||
import type { DataViewsPublicPluginStart, DataView } from '@kbn/data-views-plugin/public';
|
||||
|
@ -42,6 +42,7 @@ import type {
|
|||
import { FieldSelect } from './field_select';
|
||||
import type { Datasource, IndexPatternMap } from '../../types';
|
||||
import { LayerPanel } from './layerpanel';
|
||||
import { DimensionTrigger } from '../../shared_components/dimension_trigger';
|
||||
|
||||
function getLayerReferenceName(layerId: string) {
|
||||
return `textBasedLanguages-datasource-layer-${layerId}`;
|
||||
|
@ -377,16 +378,17 @@ export function getTextBasedDatasource({
|
|||
}
|
||||
|
||||
render(
|
||||
<EuiButtonEmpty
|
||||
<DimensionTrigger
|
||||
id={props.columnId}
|
||||
color={customLabel && selectedField ? 'primary' : 'danger'}
|
||||
onClick={() => {}}
|
||||
data-test-subj="lns-dimensionTrigger-textBased"
|
||||
>
|
||||
{customLabel ??
|
||||
dataTestSubj="lns-dimensionTrigger-textBased"
|
||||
label={
|
||||
customLabel ??
|
||||
i18n.translate('xpack.lens.textBasedLanguages.missingField', {
|
||||
defaultMessage: 'Missing field',
|
||||
})}
|
||||
</EuiButtonEmpty>,
|
||||
})
|
||||
}
|
||||
/>,
|
||||
domElement
|
||||
);
|
||||
},
|
||||
|
|
|
@ -66,13 +66,12 @@ export function AddLayerButton({
|
|||
position="bottom"
|
||||
>
|
||||
<EuiButton
|
||||
size="s"
|
||||
fullWidth
|
||||
data-test-subj="lnsLayerAddButton"
|
||||
aria-label={i18n.translate('xpack.lens.configPanel.addLayerButton', {
|
||||
defaultMessage: 'Add layer',
|
||||
})}
|
||||
fill
|
||||
color="text"
|
||||
onClick={() => onAddLayerClick(supportedLayers[0].type)}
|
||||
iconType="layers"
|
||||
>
|
||||
|
@ -89,13 +88,12 @@ export function AddLayerButton({
|
|||
data-test-subj="lnsConfigPanel__addLayerPopover"
|
||||
button={
|
||||
<EuiButton
|
||||
size="s"
|
||||
fullWidth
|
||||
data-test-subj="lnsLayerAddButton"
|
||||
aria-label={i18n.translate('xpack.lens.configPanel.addLayerButton', {
|
||||
defaultMessage: 'Add layer',
|
||||
})}
|
||||
fill
|
||||
color="text"
|
||||
onClick={() => toggleLayersChoice(!showLayersChoice)}
|
||||
iconType="layers"
|
||||
>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiButtonIcon, EuiLink, EuiToolTip } from '@elastic/eui';
|
||||
import { EuiButtonIcon, EuiLink, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { css } from '@emotion/react';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
|
@ -28,7 +28,9 @@ export function DimensionButton({
|
|||
accessorConfig,
|
||||
label,
|
||||
message,
|
||||
...otherProps // from Drag&Drop integration
|
||||
}: {
|
||||
className?: string;
|
||||
group: VisualizationDimensionGroupConfig;
|
||||
children: React.ReactElement;
|
||||
onClick: (id: string) => void;
|
||||
|
@ -38,32 +40,34 @@ export function DimensionButton({
|
|||
message: UserMessage | undefined;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<EuiToolTip
|
||||
content={message?.shortMessage || message?.longMessage || undefined}
|
||||
position="left"
|
||||
>
|
||||
<EuiLink
|
||||
className="lnsLayerPanel__dimensionLink"
|
||||
data-test-subj="lnsLayerPanel-dimensionLink"
|
||||
onClick={() => onClick(accessorConfig.columnId)}
|
||||
aria-label={triggerLinkA11yText(label)}
|
||||
title={triggerLinkA11yText(label)}
|
||||
color={
|
||||
message?.severity === 'error'
|
||||
? 'danger'
|
||||
: message?.severity === 'warning'
|
||||
? 'warning'
|
||||
: undefined
|
||||
}
|
||||
<div {...otherProps}>
|
||||
<EuiFlexGroup direction="row" alignItems="center" gutterSize="none" responsive={false}>
|
||||
<EuiFlexItem>
|
||||
<EuiToolTip
|
||||
content={message?.shortMessage || message?.longMessage || undefined}
|
||||
position="left"
|
||||
>
|
||||
<DimensionButtonIcon message={message} accessorConfig={accessorConfig}>
|
||||
{children}
|
||||
</DimensionButtonIcon>
|
||||
</EuiLink>
|
||||
</EuiToolTip>
|
||||
</div>
|
||||
<EuiLink
|
||||
className="lnsLayerPanel__dimensionLink"
|
||||
data-test-subj="lnsLayerPanel-dimensionLink"
|
||||
onClick={() => onClick(accessorConfig.columnId)}
|
||||
aria-label={triggerLinkA11yText(label)}
|
||||
title={triggerLinkA11yText(label)}
|
||||
color={
|
||||
message?.severity === 'error'
|
||||
? 'danger'
|
||||
: message?.severity === 'warning'
|
||||
? 'warning'
|
||||
: 'text'
|
||||
}
|
||||
>
|
||||
<DimensionButtonIcon message={message} accessorConfig={accessorConfig}>
|
||||
{children}
|
||||
</DimensionButtonIcon>
|
||||
</EuiLink>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiButtonIcon
|
||||
className="lnsLayerPanel__dimensionRemove"
|
||||
data-test-subj="indexPattern-dimension-remove"
|
||||
|
@ -87,6 +91,6 @@ export function DimensionButton({
|
|||
`}
|
||||
/>
|
||||
<PaletteIndicator accessorConfig={accessorConfig} />
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@ const DefaultEmptyButton = ({ columnId, group, onClick }: EmptyButtonProps) => {
|
|||
className="lnsLayerPanel__triggerText"
|
||||
color="text"
|
||||
size="s"
|
||||
iconType="plusInCircleFilled"
|
||||
iconType="plus"
|
||||
contentProps={{
|
||||
className: 'lnsLayerPanel__triggerTextContent',
|
||||
}}
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
&,
|
||||
.euiFormRow__fieldWrapper {
|
||||
& > * + * {
|
||||
margin-top: $euiSize;
|
||||
margin-top: $euiSizeS;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -64,22 +64,39 @@
|
|||
padding: $euiSizeS $euiSize;
|
||||
}
|
||||
|
||||
.lnsLayerPanel__dimensionRemove {
|
||||
margin-right: $euiSizeS;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.lnsLayerPanel__dimension {
|
||||
@include euiFontSizeS;
|
||||
border-radius: $euiBorderRadius;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
min-height: $euiSizeXXL;
|
||||
min-height: $euiSizeXL;
|
||||
position: relative;
|
||||
|
||||
// NativeRenderer is messing this up
|
||||
> div {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
.lnsLayerPanel__dimensionRemove {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transition: opacity $euiAnimSpeedFast ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lnsLayerPanel__dimension--empty {
|
||||
border: $euiBorderWidthThin dashed $euiBorderColor !important;
|
||||
|
||||
&:focus,
|
||||
&:focus-within {
|
||||
@include euiFocusRing;
|
||||
|
@ -94,28 +111,33 @@
|
|||
}
|
||||
}
|
||||
|
||||
.lnsLayerPanel__dimensionRemove {
|
||||
margin-right: $euiSizeS;
|
||||
}
|
||||
|
||||
.lnsLayerPanel__triggerText {
|
||||
width: 100%;
|
||||
padding: $euiSizeS;
|
||||
min-height: $euiSizeXXL - 2;
|
||||
padding: $euiSizeXS $euiSizeS;
|
||||
word-break: break-word;
|
||||
font-weight: $euiFontWeightRegular;
|
||||
}
|
||||
|
||||
.lnsLayerPanel__dimensionLink {
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.lnsLayerPanel__triggerTextLabel {
|
||||
transition: background-color $euiAnimSpeedFast ease-in-out;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.domDragDrop-isReplacing {
|
||||
.lnsLayerPanel__triggerText {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
|
||||
.lnsLayerPanel__triggerTextLabel {
|
||||
transition: background-color $euiAnimSpeedFast ease-in-out;
|
||||
}
|
||||
|
||||
.lnsLayerPanel__triggerTextContent {
|
||||
// Make EUI button content not centered
|
||||
justify-content: flex-start;
|
||||
|
@ -139,6 +161,7 @@
|
|||
}
|
||||
|
||||
.lnsLayerPanel__palette {
|
||||
height: $euiSizeXS / 2;
|
||||
border-radius: 0 0 ($euiBorderRadius - 1px) ($euiBorderRadius - 1px);
|
||||
|
||||
&::after {
|
||||
|
@ -153,5 +176,6 @@
|
|||
&:focus {
|
||||
@include passDownFocusRing('.lnsLayerPanel__triggerTextLabel');
|
||||
background-color: transparent;
|
||||
text-decoration-thickness: $euiBorderWidthThin !important;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -826,7 +826,8 @@ describe('LayerPanel', () => {
|
|||
const dragDropElement = instance
|
||||
.find('[data-test-subj="lnsGroup"] DragDrop')
|
||||
.first()
|
||||
.find('.lnsLayerPanel__dimension');
|
||||
.find('.lnsLayerPanel__dimension')
|
||||
.first();
|
||||
|
||||
dragDropElement.simulate('dragOver');
|
||||
dragDropElement.simulate('drop');
|
||||
|
@ -906,7 +907,7 @@ describe('LayerPanel', () => {
|
|||
|
||||
const updatedDragDropElement = instance
|
||||
.find('[data-test-subj="lnsGroupB"] DragDrop .domDragDrop')
|
||||
.at(2);
|
||||
.last();
|
||||
|
||||
updatedDragDropElement.simulate('dragOver');
|
||||
updatedDragDropElement.simulate('drop');
|
||||
|
|
|
@ -557,46 +557,45 @@ export function LayerPanel(
|
|||
onDrop={onDrop}
|
||||
indexPatterns={dataViews.indexPatterns}
|
||||
>
|
||||
<div className="lnsLayerPanel__dimension">
|
||||
<DimensionButton
|
||||
accessorConfig={accessorConfig}
|
||||
label={columnLabelMap?.[accessorConfig.columnId] ?? ''}
|
||||
group={group}
|
||||
onClick={(id: string) => {
|
||||
setActiveDimension({
|
||||
isNew: false,
|
||||
activeGroup: group,
|
||||
activeId: id,
|
||||
});
|
||||
}}
|
||||
onRemoveClick={(id: string) => {
|
||||
props.onRemoveDimension({ columnId: id, layerId });
|
||||
removeButtonRef(id);
|
||||
}}
|
||||
message={messages[0]}
|
||||
>
|
||||
{layerDatasource ? (
|
||||
<NativeRenderer
|
||||
render={layerDatasource.renderDimensionTrigger}
|
||||
nativeProps={{
|
||||
...layerDatasourceConfigProps,
|
||||
columnId: accessorConfig.columnId,
|
||||
groupId: group.groupId,
|
||||
filterOperations: group.filterOperations,
|
||||
indexPatterns: dataViews.indexPatterns,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{activeVisualization?.renderDimensionTrigger?.({
|
||||
columnId,
|
||||
label: columnLabelMap?.[columnId] ?? '',
|
||||
hideTooltip,
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</DimensionButton>
|
||||
</div>
|
||||
<DimensionButton
|
||||
className="lnsLayerPanel__dimension"
|
||||
accessorConfig={accessorConfig}
|
||||
label={columnLabelMap?.[accessorConfig.columnId] ?? ''}
|
||||
group={group}
|
||||
onClick={(id: string) => {
|
||||
setActiveDimension({
|
||||
isNew: false,
|
||||
activeGroup: group,
|
||||
activeId: id,
|
||||
});
|
||||
}}
|
||||
onRemoveClick={(id: string) => {
|
||||
props.onRemoveDimension({ columnId: id, layerId });
|
||||
removeButtonRef(id);
|
||||
}}
|
||||
message={messages[0]}
|
||||
>
|
||||
{layerDatasource ? (
|
||||
<NativeRenderer
|
||||
render={layerDatasource.renderDimensionTrigger}
|
||||
nativeProps={{
|
||||
...layerDatasourceConfigProps,
|
||||
columnId: accessorConfig.columnId,
|
||||
groupId: group.groupId,
|
||||
filterOperations: group.filterOperations,
|
||||
indexPatterns: dataViews.indexPatterns,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{activeVisualization?.renderDimensionTrigger?.({
|
||||
columnId,
|
||||
label: columnLabelMap?.[columnId] ?? '',
|
||||
hideTooltip,
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</DimensionButton>
|
||||
</DraggableDimensionButton>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import { EuiText, EuiFlexItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { EuiTextProps } from '@elastic/eui/src/components/text/text';
|
||||
|
||||
export const defaultDimensionTriggerTooltip = (
|
||||
<p>
|
||||
|
@ -20,13 +21,24 @@ export const defaultDimensionTriggerTooltip = (
|
|||
</p>
|
||||
);
|
||||
|
||||
export const DimensionTrigger = ({ id, label }: { label: string; id: string }) => {
|
||||
export const DimensionTrigger = ({
|
||||
id,
|
||||
label,
|
||||
color,
|
||||
dataTestSubj,
|
||||
}: {
|
||||
label: string;
|
||||
id: string;
|
||||
color?: EuiTextProps['color'];
|
||||
dataTestSubj?: string;
|
||||
}) => {
|
||||
return (
|
||||
<EuiText
|
||||
size="s"
|
||||
id={id}
|
||||
color={color}
|
||||
className="lnsLayerPanel__triggerText"
|
||||
data-test-subj="lns-dimensionTrigger"
|
||||
data-test-subj={dataTestSubj || 'lns-dimensionTrigger'}
|
||||
>
|
||||
<EuiFlexItem grow={true}>
|
||||
<span>
|
||||
|
|
|
@ -2147,13 +2147,10 @@
|
|||
"discover.docTable.totalDocuments": "{totalDocuments} documents",
|
||||
"discover.dscTour.stepAddFields.description": "Cliquez sur {plusIcon} pour ajouter les champs qui vous intéressent.",
|
||||
"discover.dscTour.stepExpand.description": "Cliquez sur {expandIcon} pour afficher, comparer et filtrer les documents.",
|
||||
"discover.field.title": "{fieldName} ({fieldDisplayName})",
|
||||
"discover.fieldChooser.detailViews.existsInRecordsText": "Existe dans {value} / {totalValue} enregistrements",
|
||||
"discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel": "Exclure {field} : \"{value}\"",
|
||||
"discover.fieldChooser.detailViews.filterValueButtonAriaLabel": "Filtrer sur {field} : \"{value}\"",
|
||||
"discover.fieldChooser.detailViews.valueOfRecordsText": "{value} / {totalValue} enregistrements",
|
||||
"discover.fieldChooser.discoverField.addButtonAriaLabel": "Ajouter {field} au tableau",
|
||||
"discover.fieldChooser.discoverField.removeButtonAriaLabel": "Retirer {field} du tableau",
|
||||
"discover.fieldChooser.fieldCalculator.fieldIsNotPresentInDocumentsErrorMessage": "Ce champ est présent dans votre mapping Elasticsearch, mais pas dans les {hitsLength} documents affichés dans le tableau des documents. Cependant, vous pouvez toujours le consulter ou effectuer une recherche dessus.",
|
||||
"discover.grid.copyClipboardButtonTitle": "Copier la valeur de {column}",
|
||||
"discover.grid.copyColumnValuesToClipboard.toastTitle": "Valeurs de la colonne \"{column}\" copiées dans le presse-papiers",
|
||||
|
@ -2337,8 +2334,6 @@
|
|||
"discover.embeddable.inspectorRequestDataTitle": "Données",
|
||||
"discover.embeddable.inspectorRequestDescription": "Cette requête interroge Elasticsearch afin de récupérer les données pour la recherche.",
|
||||
"discover.embeddable.search.displayName": "rechercher",
|
||||
"discover.field.mappingConflict": "Ce champ est défini avec plusieurs types (chaîne, entier, etc.) dans les différents index qui correspondent à ce modèle. Vous pouvez toujours utiliser ce champ conflictuel, mais il sera indisponible pour les fonctions qui nécessitent que Kibana en connaisse le type. Pour corriger ce problème, vous devrez réindexer vos données.",
|
||||
"discover.field.mappingConflict.title": "Conflit de mapping",
|
||||
"discover.fieldChooser.addField.label": "Ajouter un champ",
|
||||
"discover.fieldChooser.availableFieldsTooltip": "Champs disponibles pour l'affichage dans le tableau.",
|
||||
"discover.fieldChooser.detailViews.emptyStringText": "Chaîne vide",
|
||||
|
@ -19529,10 +19524,7 @@
|
|||
"xpack.lens.indexPattern.emptyFieldsLabel": "Champs vides",
|
||||
"xpack.lens.indexPattern.enableAccuracyMode": "Activer le mode de précision",
|
||||
"xpack.lens.indexPattern.fieldExploreInDiscover": "Explorer dans Discover",
|
||||
"xpack.lens.indexPattern.fieldItemTooltip": "Effectuez un glisser-déposer pour visualiser.",
|
||||
"xpack.lens.indexPattern.fieldPlaceholder": "Champ",
|
||||
"xpack.lens.indexPattern.fieldStatsButtonEmptyLabel": "Ce champ ne comporte aucune donnée mais vous pouvez toujours effectuer un glisser-déposer pour visualiser.",
|
||||
"xpack.lens.indexPattern.fieldStatsButtonLabel": "Cliquez pour obtenir un aperçu du champ, ou effectuez un glisser-déposer pour visualiser.",
|
||||
"xpack.lens.indexPattern.fieldStatsNoData": "Lens ne peut pas créer de visualisation avec ce champ, car il ne contient pas de données. Pour créer une visualisation, glissez-déposez un autre champ.",
|
||||
"xpack.lens.indexPattern.filterBy.clickToEdit": "Cliquer pour modifier",
|
||||
"xpack.lens.indexPattern.filterBy.emptyFilterQuery": "(vide)",
|
||||
|
|
|
@ -2147,13 +2147,10 @@
|
|||
"discover.docTable.totalDocuments": "{totalDocuments}ドキュメント",
|
||||
"discover.dscTour.stepAddFields.description": "{plusIcon}をクリックして、関心があるフィールドを追加します。",
|
||||
"discover.dscTour.stepExpand.description": "{expandIcon}をクリックすると、ドキュメントを表示、比較、フィルタリングできます。",
|
||||
"discover.field.title": "{fieldName}({fieldDisplayName})",
|
||||
"discover.fieldChooser.detailViews.existsInRecordsText": "{value} / {totalValue}レコードに存在します",
|
||||
"discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel": "{field}を除外:\"{value}\"",
|
||||
"discover.fieldChooser.detailViews.filterValueButtonAriaLabel": "{field}のフィルター:\"{value}\"",
|
||||
"discover.fieldChooser.detailViews.valueOfRecordsText": "{value} / {totalValue}レコード",
|
||||
"discover.fieldChooser.discoverField.addButtonAriaLabel": "{field}をテーブルに追加",
|
||||
"discover.fieldChooser.discoverField.removeButtonAriaLabel": "{field}をテーブルから削除",
|
||||
"discover.fieldChooser.fieldCalculator.fieldIsNotPresentInDocumentsErrorMessage": "このフィールドはElasticsearchマッピングに表示されますが、ドキュメントテーブルの{hitsLength}件のドキュメントには含まれません。可視化や検索は可能な場合があります。",
|
||||
"discover.grid.copyClipboardButtonTitle": "{column}の値をコピー",
|
||||
"discover.grid.copyColumnValuesToClipboard.toastTitle": "\"{column}\"列の値がクリップボードにコピーされました",
|
||||
|
@ -2337,8 +2334,6 @@
|
|||
"discover.embeddable.inspectorRequestDataTitle": "データ",
|
||||
"discover.embeddable.inspectorRequestDescription": "このリクエストはElasticsearchにクエリをかけ、検索データを取得します。",
|
||||
"discover.embeddable.search.displayName": "検索",
|
||||
"discover.field.mappingConflict": "このフィールドは、このパターンと一致するインデックス全体に対して複数の型(文字列、整数など)として定義されています。この競合フィールドを使用することはできますが、Kibana で型を認識する必要がある関数では使用できません。この問題を修正するにはデータのレンダリングが必要です。",
|
||||
"discover.field.mappingConflict.title": "マッピングの矛盾",
|
||||
"discover.fieldChooser.addField.label": "フィールドを追加",
|
||||
"discover.fieldChooser.availableFieldsTooltip": "フィールドをテーブルに表示できます。",
|
||||
"discover.fieldChooser.detailViews.emptyStringText": "空の文字列",
|
||||
|
@ -19529,10 +19524,7 @@
|
|||
"xpack.lens.indexPattern.emptyFieldsLabel": "空のフィールド",
|
||||
"xpack.lens.indexPattern.enableAccuracyMode": "精度モードを有効にする",
|
||||
"xpack.lens.indexPattern.fieldExploreInDiscover": "Discoverで探索",
|
||||
"xpack.lens.indexPattern.fieldItemTooltip": "可視化するには、ドラッグアンドドロップします。",
|
||||
"xpack.lens.indexPattern.fieldPlaceholder": "フィールド",
|
||||
"xpack.lens.indexPattern.fieldStatsButtonEmptyLabel": "このフィールドにはデータがありませんが、ドラッグアンドドロップで可視化できます。",
|
||||
"xpack.lens.indexPattern.fieldStatsButtonLabel": "フィールドプレビューを表示するには、クリックします。可視化するには、ドラッグアンドドロップします。",
|
||||
"xpack.lens.indexPattern.fieldStatsNoData": "Lensはこのフィールドのビジュアライゼーションを作成できません。フィールドにデータがありません。ビジュアライゼーションを作成するには、別のフィールドをドラッグします。",
|
||||
"xpack.lens.indexPattern.filterBy.clickToEdit": "クリックして編集",
|
||||
"xpack.lens.indexPattern.filterBy.emptyFilterQuery": "(空)",
|
||||
|
|
|
@ -2147,13 +2147,10 @@
|
|||
"discover.docTable.totalDocuments": "{totalDocuments} 个文档",
|
||||
"discover.dscTour.stepAddFields.description": "单击 {plusIcon} 以添加您感兴趣的字段。",
|
||||
"discover.dscTour.stepExpand.description": "单击 {expandIcon} 以查看、比较和筛选文档。",
|
||||
"discover.field.title": "{fieldName} ({fieldDisplayName})",
|
||||
"discover.fieldChooser.detailViews.existsInRecordsText": "存在于 {value}/{totalValue} 条记录中",
|
||||
"discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel": "筛除 {field}“{value}”",
|
||||
"discover.fieldChooser.detailViews.filterValueButtonAriaLabel": "筛留 {field}“{value}”",
|
||||
"discover.fieldChooser.detailViews.valueOfRecordsText": "{value}/{totalValue} 条记录",
|
||||
"discover.fieldChooser.discoverField.addButtonAriaLabel": "将 {field} 添加到表中",
|
||||
"discover.fieldChooser.discoverField.removeButtonAriaLabel": "从表中移除 {field}",
|
||||
"discover.fieldChooser.fieldCalculator.fieldIsNotPresentInDocumentsErrorMessage": "此字段在您的 Elasticsearch 映射中,但不在文档表中显示的 {hitsLength} 个文档中。您可能仍能够基于它可视化或搜索。",
|
||||
"discover.grid.copyClipboardButtonTitle": "复制 {column} 的值",
|
||||
"discover.grid.copyColumnValuesToClipboard.toastTitle": "“{column}”列的值已复制到剪贴板",
|
||||
|
@ -2337,8 +2334,6 @@
|
|||
"discover.embeddable.inspectorRequestDataTitle": "数据",
|
||||
"discover.embeddable.inspectorRequestDescription": "此请求将查询 Elasticsearch 以获取搜索的数据。",
|
||||
"discover.embeddable.search.displayName": "搜索",
|
||||
"discover.field.mappingConflict": "此字段在匹配此模式的各个索引中已定义为若干类型(字符串、整数等)。您可能仍可以使用此冲突字段,但它无法用于需要 Kibana 知道其类型的函数。要解决此问题,需要重新索引您的数据。",
|
||||
"discover.field.mappingConflict.title": "映射冲突",
|
||||
"discover.fieldChooser.addField.label": "添加字段",
|
||||
"discover.fieldChooser.availableFieldsTooltip": "适用于在表中显示的字段。",
|
||||
"discover.fieldChooser.detailViews.emptyStringText": "空字符串",
|
||||
|
@ -19530,10 +19525,7 @@
|
|||
"xpack.lens.indexPattern.emptyFieldsLabel": "空字段",
|
||||
"xpack.lens.indexPattern.enableAccuracyMode": "启用准确性模式",
|
||||
"xpack.lens.indexPattern.fieldExploreInDiscover": "在 Discover 中浏览",
|
||||
"xpack.lens.indexPattern.fieldItemTooltip": "拖放以可视化。",
|
||||
"xpack.lens.indexPattern.fieldPlaceholder": "字段",
|
||||
"xpack.lens.indexPattern.fieldStatsButtonEmptyLabel": "此字段不包含任何数据,但您仍然可以拖放以进行可视化。",
|
||||
"xpack.lens.indexPattern.fieldStatsButtonLabel": "单击以进行字段预览,或拖放以进行可视化。",
|
||||
"xpack.lens.indexPattern.fieldStatsNoData": "Lens 无法使用此字段创建可视化,因为其中未包含数据。要创建可视化,请拖放其他字段。",
|
||||
"xpack.lens.indexPattern.filterBy.clickToEdit": "单击以编辑",
|
||||
"xpack.lens.indexPattern.filterBy.emptyFilterQuery": "(空)",
|
||||
|
|
|
@ -148,7 +148,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await listingTable.searchForItemWithName('lnsXYvis');
|
||||
await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis');
|
||||
await PageObjects.lens.goToTimeRange();
|
||||
await testSubjects.click('lnsXY_splitDimensionPanel > indexPattern-dimension-remove');
|
||||
await PageObjects.lens.removeDimension('lnsXY_splitDimensionPanel');
|
||||
await PageObjects.lens.switchToVisualization('line');
|
||||
await PageObjects.lens.configureDimension({
|
||||
dimension: 'lnsXY_yDimensionPanel > lns-dimensionTrigger',
|
||||
|
@ -200,9 +200,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.eql(
|
||||
longLabel
|
||||
);
|
||||
expect(
|
||||
await testSubjects.isDisplayed('lnsXY_yDimensionPanel > indexPattern-dimension-remove')
|
||||
).to.equal(true);
|
||||
expect(await PageObjects.lens.canRemoveDimension('lnsXY_yDimensionPanel')).to.equal(true);
|
||||
await PageObjects.lens.removeDimension('lnsXY_yDimensionPanel');
|
||||
await testSubjects.missingOrFail('lnsXY_yDimensionPanel > lns-dimensionTrigger');
|
||||
});
|
||||
|
|
|
@ -626,10 +626,18 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
|
|||
async isTopLevelAggregation() {
|
||||
return await testSubjects.isEuiSwitchChecked('indexPattern-nesting-switch');
|
||||
},
|
||||
/**
|
||||
* Cen remove the dimension matching a specific test subject?
|
||||
*/
|
||||
async canRemoveDimension(dimensionTestSubj: string) {
|
||||
await testSubjects.moveMouseTo(`${dimensionTestSubj} > indexPattern-dimension-remove`);
|
||||
return await testSubjects.isDisplayed(`${dimensionTestSubj} > indexPattern-dimension-remove`);
|
||||
},
|
||||
/**
|
||||
* Removes the dimension matching a specific test subject
|
||||
*/
|
||||
async removeDimension(dimensionTestSubj: string) {
|
||||
await testSubjects.moveMouseTo(`${dimensionTestSubj} > indexPattern-dimension-remove`);
|
||||
await testSubjects.click(`${dimensionTestSubj} > indexPattern-dimension-remove`);
|
||||
},
|
||||
/**
|
||||
|
@ -1663,7 +1671,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
|
|||
) {
|
||||
const groupCapitalized = `${group[0].toUpperCase()}${group.slice(1).toLowerCase()}`;
|
||||
const allFieldsForType = await find.allByCssSelector(
|
||||
`[data-test-subj="lnsIndexPattern${groupCapitalized}Fields"] .unifiedFieldItemButton--${type}`
|
||||
`[data-test-subj="lnsIndexPattern${groupCapitalized}Fields"] .unifiedFieldListItemButton--${type}`
|
||||
);
|
||||
// map to testSubjId
|
||||
return Promise.all(allFieldsForType.map((el) => el.getAttribute('data-test-subj')));
|
||||
|
|
|
@ -1052,9 +1052,7 @@ export function TransformWizardProvider({ getService, getPageObjects }: FtrProvi
|
|||
await retry.tryForTime(60 * 1000, async () => {
|
||||
const allFields = await pageObjects.discover.getAllFieldNames();
|
||||
if (Array.isArray(allFields)) {
|
||||
// For some reasons, Discover returns fields with dot (e.g '.avg') with extra space
|
||||
const fields = allFields.map((n) => n.replace('.', '.'));
|
||||
expect(fields).to.contain(
|
||||
expect(allFields).to.contain(
|
||||
field,
|
||||
`Expected Discover to contain field ${field}, got ${allFields.join()}`
|
||||
);
|
||||
|
|
|
@ -292,7 +292,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await listingTable.searchForItemWithName('lnsXYvis');
|
||||
await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis');
|
||||
await PageObjects.lens.goToTimeRange();
|
||||
await testSubjects.click('lnsXY_splitDimensionPanel > indexPattern-dimension-remove');
|
||||
await PageObjects.lens.removeDimension('lnsXY_splitDimensionPanel');
|
||||
await PageObjects.lens.switchToVisualization('line', termTranslator('line'));
|
||||
await PageObjects.lens.configureDimension({
|
||||
dimension: 'lnsXY_yDimensionPanel > lns-dimensionTrigger',
|
||||
|
@ -344,9 +344,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel')).to.eql(
|
||||
longLabel
|
||||
);
|
||||
expect(
|
||||
await testSubjects.isDisplayed('lnsXY_yDimensionPanel > indexPattern-dimension-remove')
|
||||
).to.equal(true);
|
||||
expect(await PageObjects.lens.canRemoveDimension('lnsXY_yDimensionPanel')).to.equal(true);
|
||||
await PageObjects.lens.removeDimension('lnsXY_yDimensionPanel');
|
||||
await testSubjects.missingOrFail('lnsXY_yDimensionPanel > lns-dimensionTrigger');
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue