mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Discover/Lens] Unify field popover (#141787)
* [UnifiedFieldList] Add a new FieldPopover component * [Discover] Integrate FieldPopover into Discover page * [Discover] Integrate FieldPopover into Lens page * [Discover] Update for tests * [Discover] Add "Add field as column" action to Discover * [Discover] Deprecate i18n keys * [Discover] Update tests * [Discover] Fix field type * [Discover] Fix sidebar pagination when lazy js modules are being loaded * [Discover] Extract new FieldVisualizeButton * [Discover] Integrate it into Lens * [Discover] Fix checks * [Discover] Add popover tests * [Discover] Add popover header tests * [Discover] Add field visualize button tests * [Discover] Update docs * [Discover] Update tests * [Discover] Fix button state on rerender * [Discover] Reorganize components * [Discover] Update tests * Merge remote-tracking branch 'upstream/main' into 140363-unified-popover # Conflicts: # x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx * [Discover] Fix for ad hoc data views * [Discover] Fix multi fields toggle action * [Discover] Fix display name in the popover * [Discover] A fix for tests * [Discover] Update the icon to plusInCircle * [UnifiedFieldList] Remove redundant styles Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
This commit is contained in:
parent
d7924aa750
commit
91dbdc7888
40 changed files with 1268 additions and 629 deletions
|
@ -18,16 +18,18 @@ import {
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { SavedSearch } from '@kbn/saved-search-plugin/public';
|
||||
import {
|
||||
getVisualizeInformation,
|
||||
triggerVisualizeActions,
|
||||
} from '@kbn/unified-field-list-plugin/public';
|
||||
import { HitsCounter } from '../hits_counter';
|
||||
import { GetStateReturn } from '../../services/discover_state';
|
||||
import { DiscoverHistogram } from './histogram';
|
||||
import { DataCharts$, DataTotalHits$ } from '../../hooks/use_saved_search';
|
||||
import { useChartPanels } from './use_chart_panels';
|
||||
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
|
||||
import {
|
||||
getVisualizeInformation,
|
||||
triggerVisualizeActions,
|
||||
} from '../sidebar/lib/visualize_trigger_utils';
|
||||
import { getUiActions } from '../../../../kibana_services';
|
||||
import { PLUGIN_ID } from '../../../../../common';
|
||||
|
||||
const DiscoverHistogramMemoized = memo(DiscoverHistogram);
|
||||
export const CHART_HIDDEN_KEY = 'discover:chartHidden';
|
||||
|
@ -72,7 +74,13 @@ export function DiscoverChart({
|
|||
|
||||
useEffect(() => {
|
||||
if (!timeField) return;
|
||||
getVisualizeInformation(timeField, dataView, savedSearch.columns || []).then((info) => {
|
||||
getVisualizeInformation(
|
||||
getUiActions(),
|
||||
timeField,
|
||||
dataView,
|
||||
savedSearch.columns || [],
|
||||
[]
|
||||
).then((info) => {
|
||||
setCanVisualize(Boolean(info));
|
||||
});
|
||||
}, [dataView, savedSearch.columns, timeField]);
|
||||
|
@ -81,7 +89,13 @@ export function DiscoverChart({
|
|||
if (!timeField) {
|
||||
return;
|
||||
}
|
||||
triggerVisualizeActions(timeField, savedSearch.columns || [], dataView);
|
||||
triggerVisualizeActions(
|
||||
getUiActions(),
|
||||
timeField,
|
||||
savedSearch.columns || [],
|
||||
PLUGIN_ID,
|
||||
dataView
|
||||
);
|
||||
}, [dataView, savedSearch.columns, timeField]);
|
||||
|
||||
const onShowChartOptions = useCallback(() => {
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
/*
|
||||
* 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 { storiesOf } from '@storybook/react';
|
||||
import React from 'react';
|
||||
import { DiscoverFieldVisualizeInner } from '../discover_field_visualize_inner';
|
||||
import { numericField as field } from './fields';
|
||||
|
||||
const visualizeInfo = {
|
||||
href: 'http://localhost:9001/',
|
||||
field,
|
||||
};
|
||||
|
||||
const handleVisualizeLinkClick = () => {
|
||||
alert('Clicked');
|
||||
};
|
||||
|
||||
storiesOf('components/sidebar/DiscoverFieldVisualizeInner', module).add('default', () => (
|
||||
<DiscoverFieldVisualizeInner
|
||||
field={field}
|
||||
visualizeInfo={visualizeInfo}
|
||||
handleVisualizeLinkClick={handleVisualizeLinkClick}
|
||||
/>
|
||||
));
|
|
@ -1,8 +1,3 @@
|
|||
.dscSidebarItem__fieldPopoverPanel {
|
||||
min-width: $euiSizeXXL * 6.5;
|
||||
max-width: $euiSizeXXL * 7.5;
|
||||
}
|
||||
|
||||
.dscSidebarItem--multi {
|
||||
.kbnFieldButton__button {
|
||||
padding-left: 0;
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { EuiPopover, EuiProgress, EuiButtonIcon } from '@elastic/eui';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
|
@ -85,6 +84,7 @@ async function getComponent({
|
|||
getDetails: jest.fn(() => ({ buckets: [], error: '', exists: 1, total: 2 })),
|
||||
...(onAddFilterExists && { onAddFilter: jest.fn() }),
|
||||
onAddField: jest.fn(),
|
||||
onEditField: jest.fn(),
|
||||
onRemoveField: jest.fn(),
|
||||
showFieldStats,
|
||||
selected,
|
||||
|
@ -140,6 +140,9 @@ async function getComponent({
|
|||
<DiscoverField {...props} />
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
// wait for lazy modules
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
await comp.update();
|
||||
return { comp, props };
|
||||
}
|
||||
|
||||
|
@ -233,13 +236,8 @@ describe('discover sidebar field', function () {
|
|||
aggregatable: true,
|
||||
searchable: true,
|
||||
});
|
||||
let comp: ReactWrapper;
|
||||
|
||||
await act(async () => {
|
||||
const result = await getComponent({ showFieldStats: true, field, onAddFilterExists: true });
|
||||
comp = result.comp;
|
||||
await comp.update();
|
||||
});
|
||||
const { comp } = await getComponent({ showFieldStats: true, field, onAddFilterExists: true });
|
||||
|
||||
await act(async () => {
|
||||
const fieldItem = findTestSubject(comp, 'field-machine.os.raw-showDetails');
|
||||
|
@ -247,15 +245,91 @@ describe('discover sidebar field', function () {
|
|||
await comp.update();
|
||||
});
|
||||
|
||||
await comp!.update();
|
||||
await comp.update();
|
||||
|
||||
expect(comp!.find(EuiPopover).prop('isOpen')).toBe(true);
|
||||
expect(findTestSubject(comp!, 'dscFieldStats-title').text()).toBe('Top values');
|
||||
expect(findTestSubject(comp!, 'dscFieldStats-topValues-bucket')).toHaveLength(2);
|
||||
expect(comp.find(EuiPopover).prop('isOpen')).toBe(true);
|
||||
expect(findTestSubject(comp, 'dscFieldStats-title').text()).toBe('Top values');
|
||||
expect(findTestSubject(comp, 'dscFieldStats-topValues-bucket')).toHaveLength(2);
|
||||
expect(
|
||||
findTestSubject(comp!, 'dscFieldStats-topValues-formattedFieldValue').first().text()
|
||||
findTestSubject(comp, 'dscFieldStats-topValues-formattedFieldValue').first().text()
|
||||
).toBe('osx');
|
||||
expect(comp!.find(EuiProgress)).toHaveLength(2);
|
||||
expect(findTestSubject(comp!, 'dscFieldStats-topValues').find(EuiButtonIcon)).toHaveLength(4);
|
||||
expect(comp.find(EuiProgress)).toHaveLength(2);
|
||||
expect(findTestSubject(comp, 'dscFieldStats-topValues').find(EuiButtonIcon)).toHaveLength(4);
|
||||
});
|
||||
it('should include popover actions', async function () {
|
||||
const field = new DataViewField({
|
||||
name: 'extension.keyword',
|
||||
type: 'string',
|
||||
esTypes: ['keyword'],
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
});
|
||||
|
||||
const { comp, props } = await getComponent({ field, onAddFilterExists: true });
|
||||
|
||||
await act(async () => {
|
||||
const fieldItem = findTestSubject(comp, 'field-extension.keyword-showDetails');
|
||||
await fieldItem.simulate('click');
|
||||
await comp.update();
|
||||
});
|
||||
|
||||
await comp.update();
|
||||
|
||||
expect(comp.find(EuiPopover).prop('isOpen')).toBe(true);
|
||||
expect(
|
||||
comp.find('[data-test-subj="fieldPopoverHeader_addField-extension.keyword"]').exists()
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
comp
|
||||
.find('[data-test-subj="discoverFieldListPanelAddExistFilter-extension.keyword"]')
|
||||
.exists()
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
comp.find('[data-test-subj="discoverFieldListPanelEdit-extension.keyword"]').exists()
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
comp.find('[data-test-subj="discoverFieldListPanelDelete-extension.keyword"]').exists()
|
||||
).toBeFalsy();
|
||||
|
||||
await act(async () => {
|
||||
const fieldItem = findTestSubject(comp, 'fieldPopoverHeader_addField-extension.keyword');
|
||||
await fieldItem.simulate('click');
|
||||
await comp.update();
|
||||
});
|
||||
|
||||
expect(props.onAddField).toHaveBeenCalledWith('extension.keyword');
|
||||
|
||||
await comp.update();
|
||||
|
||||
expect(comp.find(EuiPopover).prop('isOpen')).toBe(false);
|
||||
});
|
||||
|
||||
it('should not include + action for selected fields', async function () {
|
||||
const field = new DataViewField({
|
||||
name: 'extension.keyword',
|
||||
type: 'string',
|
||||
esTypes: ['keyword'],
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
});
|
||||
|
||||
const { comp } = await getComponent({
|
||||
field,
|
||||
onAddFilterExists: true,
|
||||
selected: true,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const fieldItem = findTestSubject(comp, 'field-extension.keyword-showDetails');
|
||||
await fieldItem.simulate('click');
|
||||
await comp.update();
|
||||
});
|
||||
|
||||
await comp.update();
|
||||
|
||||
expect(comp.find(EuiPopover).prop('isOpen')).toBe(true);
|
||||
expect(
|
||||
comp.find('[data-test-subj="fieldPopoverHeader_addField-extension.keyword"]').exists()
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,32 +9,27 @@
|
|||
import './discover_field.scss';
|
||||
|
||||
import React, { useState, useCallback, memo, useMemo } from 'react';
|
||||
import {
|
||||
EuiPopover,
|
||||
EuiPopoverTitle,
|
||||
EuiButtonIcon,
|
||||
EuiToolTip,
|
||||
EuiTitle,
|
||||
EuiIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { EuiButtonIcon, EuiToolTip, EuiTitle, EuiIcon, EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { UiCounterMetricType } from '@kbn/analytics';
|
||||
import classNames from 'classnames';
|
||||
import { FieldButton, FieldIcon } from '@kbn/react-field';
|
||||
import type { DataViewField, DataView } from '@kbn/data-views-plugin/public';
|
||||
import { FieldStats } from '@kbn/unified-field-list-plugin/public';
|
||||
import { getFieldCapabilities } from '../../../../utils/get_field_capabilities';
|
||||
import {
|
||||
FieldStats,
|
||||
FieldPopover,
|
||||
FieldPopoverHeader,
|
||||
FieldPopoverHeaderProps,
|
||||
FieldPopoverVisualize,
|
||||
} from '@kbn/unified-field-list-plugin/public';
|
||||
import { getTypeForFieldIcon } from '../../../../utils/get_type_for_field_icon';
|
||||
import { DiscoverFieldDetails } from './discover_field_details';
|
||||
import { FieldDetails } from './types';
|
||||
import { getFieldTypeName } from '../../../../utils/get_field_type_name';
|
||||
import { DiscoverFieldVisualize } from './discover_field_visualize';
|
||||
import type { AppState } from '../../services/discover_state';
|
||||
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
|
||||
import { SHOW_LEGACY_FIELD_TOP_VALUES } from '../../../../../common';
|
||||
import { SHOW_LEGACY_FIELD_TOP_VALUES, PLUGIN_ID } from '../../../../../common';
|
||||
import { getUiActions } from '../../../../kibana_services';
|
||||
|
||||
function wrapOnDot(str?: string) {
|
||||
// u200B is a non-width white-space character, which allows
|
||||
|
@ -93,7 +88,7 @@ interface ActionButtonProps {
|
|||
field: DataViewField;
|
||||
isSelected?: boolean;
|
||||
alwaysShow: boolean;
|
||||
toggleDisplay: (field: DataViewField) => void;
|
||||
toggleDisplay: (field: DataViewField, isSelected?: boolean) => void;
|
||||
}
|
||||
|
||||
const ActionButton: React.FC<ActionButtonProps> = memo(
|
||||
|
@ -121,7 +116,7 @@ const ActionButton: React.FC<ActionButtonProps> = memo(
|
|||
}
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
toggleDisplay(field);
|
||||
toggleDisplay(field, isSelected);
|
||||
}}
|
||||
data-test-subj={`fieldToggle-${field.name}`}
|
||||
aria-label={i18n.translate('discover.fieldChooser.discoverField.addButtonAriaLabel', {
|
||||
|
@ -149,7 +144,7 @@ const ActionButton: React.FC<ActionButtonProps> = memo(
|
|||
}
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
toggleDisplay(field);
|
||||
toggleDisplay(field, isSelected);
|
||||
}}
|
||||
data-test-subj={`fieldToggle-${field.name}`}
|
||||
aria-label={i18n.translate(
|
||||
|
@ -311,23 +306,48 @@ function DiscoverFieldComponent({
|
|||
[setOpen, onAddFilter]
|
||||
);
|
||||
|
||||
const toggleDisplay = useCallback(
|
||||
(f: DataViewField) => {
|
||||
if (selected) {
|
||||
const togglePopover = useCallback(() => {
|
||||
setOpen((value) => !value);
|
||||
}, [setOpen]);
|
||||
|
||||
const closePopover = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, [setOpen]);
|
||||
|
||||
const toggleDisplay: ActionButtonProps['toggleDisplay'] = useCallback(
|
||||
(f, isCurrentlySelected) => {
|
||||
closePopover();
|
||||
if (isCurrentlySelected) {
|
||||
onRemoveField(f.name);
|
||||
} else {
|
||||
onAddField(f.name);
|
||||
}
|
||||
},
|
||||
[onAddField, onRemoveField, selected]
|
||||
[onAddField, onRemoveField, closePopover]
|
||||
);
|
||||
|
||||
const togglePopover = useCallback(() => {
|
||||
setOpen(!infoIsOpen);
|
||||
}, [infoIsOpen]);
|
||||
|
||||
const rawMultiFields = useMemo(() => multiFields?.map((f) => f.field), [multiFields]);
|
||||
|
||||
const customPopoverHeaderProps: Partial<FieldPopoverHeaderProps> = useMemo(
|
||||
() => ({
|
||||
buttonAddFieldToWorkspaceProps: {
|
||||
'aria-label': i18n.translate('discover.fieldChooser.discoverField.addFieldTooltip', {
|
||||
defaultMessage: 'Add field as column',
|
||||
}),
|
||||
},
|
||||
buttonAddFilterProps: {
|
||||
'data-test-subj': `discoverFieldListPanelAddExistFilter-${field.name}`,
|
||||
},
|
||||
buttonEditFieldProps: {
|
||||
'data-test-subj': `discoverFieldListPanelEdit-${field.name}`,
|
||||
},
|
||||
buttonDeleteFieldProps: {
|
||||
'data-test-subj': `discoverFieldListPanelDelete-${field.name}`,
|
||||
},
|
||||
}),
|
||||
[field.name]
|
||||
);
|
||||
|
||||
if (field.type === '_source') {
|
||||
return (
|
||||
<FieldButton
|
||||
|
@ -348,85 +368,6 @@ function DiscoverFieldComponent({
|
|||
);
|
||||
}
|
||||
|
||||
const { canEdit, canDelete } = getFieldCapabilities(dataView, field);
|
||||
const canEditField = onEditField && canEdit;
|
||||
const canDeleteField = onDeleteField && canDelete;
|
||||
|
||||
const addExistFilterTooltip = i18n.translate(
|
||||
'discover.fieldChooser.discoverField.addExistFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Filter for field present',
|
||||
}
|
||||
);
|
||||
|
||||
const editFieldTooltip = i18n.translate('discover.fieldChooser.discoverField.editFieldLabel', {
|
||||
defaultMessage: 'Edit data view field',
|
||||
});
|
||||
|
||||
const deleteFieldTooltip = i18n.translate(
|
||||
'discover.fieldChooser.discoverField.deleteFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Delete data view field',
|
||||
}
|
||||
);
|
||||
|
||||
const popoverTitle = (
|
||||
<EuiPopoverTitle style={{ textTransform: 'none' }} className="eui-textBreakWord">
|
||||
<EuiFlexGroup responsive={false} gutterSize="s">
|
||||
<EuiFlexItem grow={true}>
|
||||
<h5>{field.displayName}</h5>
|
||||
</EuiFlexItem>
|
||||
{onAddFilter && !dataView.metaFields.includes(field.name) && !field.scripted && (
|
||||
<EuiFlexItem grow={false} data-test-subj="discoverFieldListPanelAddExistFilterItem">
|
||||
<EuiToolTip content={addExistFilterTooltip}>
|
||||
<EuiButtonIcon
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
onAddFilter('_exists_', field.name, '+');
|
||||
}}
|
||||
iconType="filter"
|
||||
data-test-subj={`discoverFieldListPanelAddExistFilter-${field.name}`}
|
||||
aria-label={addExistFilterTooltip}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{canEditField && (
|
||||
<EuiFlexItem grow={false} data-test-subj="discoverFieldListPanelEditItem">
|
||||
<EuiToolTip content={editFieldTooltip}>
|
||||
<EuiButtonIcon
|
||||
onClick={() => {
|
||||
if (onEditField) {
|
||||
togglePopover();
|
||||
onEditField(field.name);
|
||||
}
|
||||
}}
|
||||
iconType="pencil"
|
||||
data-test-subj={`discoverFieldListPanelEdit-${field.name}`}
|
||||
aria-label={editFieldTooltip}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{canDeleteField && (
|
||||
<EuiFlexItem grow={false} data-test-subj="discoverFieldListPanelDeleteItem">
|
||||
<EuiToolTip content={deleteFieldTooltip}>
|
||||
<EuiButtonIcon
|
||||
onClick={() => {
|
||||
onDeleteField?.(field.name);
|
||||
}}
|
||||
iconType="trash"
|
||||
data-test-subj={`discoverFieldListPanelDelete-${field.name}`}
|
||||
color="danger"
|
||||
aria-label={deleteFieldTooltip}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiPopoverTitle>
|
||||
);
|
||||
|
||||
const button = (
|
||||
<FieldButton
|
||||
size="s"
|
||||
|
@ -447,6 +388,7 @@ function DiscoverFieldComponent({
|
|||
fieldInfoIcon={field.type === 'conflict' && <FieldInfoIcon />}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!isDocumentRecord) {
|
||||
return button;
|
||||
}
|
||||
|
@ -512,29 +454,38 @@ function DiscoverFieldComponent({
|
|||
</>
|
||||
)}
|
||||
|
||||
<DiscoverFieldVisualize
|
||||
<FieldPopoverVisualize
|
||||
field={field}
|
||||
dataView={dataView}
|
||||
multiFields={rawMultiFields}
|
||||
trackUiMetric={trackUiMetric}
|
||||
contextualFields={contextualFields}
|
||||
originatingApp={PLUGIN_ID}
|
||||
uiActions={getUiActions()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
display="block"
|
||||
button={button}
|
||||
<FieldPopover
|
||||
isOpen={infoIsOpen}
|
||||
closePopover={() => setOpen(false)}
|
||||
button={button}
|
||||
closePopover={closePopover}
|
||||
data-test-subj="discoverFieldListPanelPopover"
|
||||
anchorPosition="rightUp"
|
||||
panelClassName="dscSidebarItem__fieldPopoverPanel"
|
||||
>
|
||||
{popoverTitle}
|
||||
{infoIsOpen && renderPopover()}
|
||||
</EuiPopover>
|
||||
renderHeader={() => (
|
||||
<FieldPopoverHeader
|
||||
field={field}
|
||||
closePopover={closePopover}
|
||||
onAddFieldToWorkspace={!selected ? toggleDisplay : undefined}
|
||||
onAddFilter={onAddFilter}
|
||||
onEditField={onEditField}
|
||||
onDeleteField={onDeleteField}
|
||||
{...customPopoverHeaderProps}
|
||||
/>
|
||||
)}
|
||||
renderContent={renderPopover}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -85,7 +85,7 @@ describe('discover sidebar', function () {
|
|||
let props: DiscoverSidebarProps;
|
||||
let comp: ReactWrapper<DiscoverSidebarProps>;
|
||||
|
||||
beforeAll(() => {
|
||||
beforeAll(async () => {
|
||||
props = getCompProps();
|
||||
mockDiscoverServices.data.dataViews.getIdsWithTitle = jest
|
||||
.fn()
|
||||
|
@ -95,11 +95,14 @@ describe('discover sidebar', function () {
|
|||
return { ...dataView, isPersisted: () => true };
|
||||
});
|
||||
|
||||
comp = mountWithIntl(
|
||||
comp = await mountWithIntl(
|
||||
<KibanaContextProvider services={mockDiscoverServices}>
|
||||
<DiscoverSidebar {...props} />
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
// wait for lazy modules
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
await comp.update();
|
||||
});
|
||||
|
||||
it('should have Selected Fields and Available Fields with Popular Fields sections', function () {
|
||||
|
@ -126,8 +129,10 @@ describe('discover sidebar', function () {
|
|||
expect(props.editField).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it('should render "Edit field" button', () => {
|
||||
it('should render "Edit field" button', async () => {
|
||||
findTestSubject(comp, 'field-bytes').simulate('click');
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
await comp.update();
|
||||
const editFieldButton = findTestSubject(comp, 'discoverFieldListPanelEdit-bytes');
|
||||
expect(editFieldButton.length).toBe(1);
|
||||
editFieldButton.simulate('click');
|
||||
|
|
|
@ -28,10 +28,11 @@ import { isEqual } from 'lodash';
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { DataViewPicker } from '@kbn/unified-search-plugin/public';
|
||||
import { DataViewField, getFieldSubtypeMulti } from '@kbn/data-views-plugin/public';
|
||||
import { triggerVisualizeActionsTextBasedLanguages } from '@kbn/unified-field-list-plugin/public';
|
||||
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
|
||||
import { DiscoverField } from './discover_field';
|
||||
import { DiscoverFieldSearch } from './discover_field_search';
|
||||
import { FIELDS_LIMIT_SETTING } from '../../../../../common';
|
||||
import { FIELDS_LIMIT_SETTING, PLUGIN_ID } from '../../../../../common';
|
||||
import { groupFields } from './lib/group_fields';
|
||||
import { getDetails } from './lib/get_details';
|
||||
import { FieldFilterState, getDefaultFieldFilter, setFieldFilterProp } from './lib/field_filter';
|
||||
|
@ -40,7 +41,7 @@ import { DiscoverSidebarResponsiveProps } from './discover_sidebar_responsive';
|
|||
import { VIEW_MODE } from '../../../../components/view_mode_toggle';
|
||||
import { DISCOVER_TOUR_STEP_ANCHOR_IDS } from '../../../../components/discover_tour';
|
||||
import type { DataTableRecord } from '../../../../types';
|
||||
import { triggerVisualizeActionsTextBasedLanguages } from './lib/visualize_trigger_utils';
|
||||
import { getUiActions } from '../../../../kibana_services';
|
||||
|
||||
/**
|
||||
* Default number of available fields displayed and added on scroll
|
||||
|
@ -187,7 +188,8 @@ export function DiscoverSidebarComponent({
|
|||
// In this case the fieldsPerPage needs to be adapted
|
||||
const fieldsRenderedHeight = availableFieldsContainer.current.clientHeight;
|
||||
const avgHeightPerItem = Math.round(fieldsRenderedHeight / fieldsToRender);
|
||||
const newFieldsPerPage = Math.round(clientHeight / avgHeightPerItem) + 10;
|
||||
const newFieldsPerPage =
|
||||
(avgHeightPerItem > 0 ? Math.round(clientHeight / avgHeightPerItem) : 0) + 10;
|
||||
if (newFieldsPerPage >= FIELDS_PER_PAGE && newFieldsPerPage !== fieldsPerPage) {
|
||||
setFieldsPerPage(newFieldsPerPage);
|
||||
setFieldsToRender(newFieldsPerPage);
|
||||
|
@ -314,7 +316,13 @@ export function DiscoverSidebarComponent({
|
|||
const visualizeAggregateQuery = useCallback(() => {
|
||||
const aggregateQuery =
|
||||
state.query && isOfAggregateQueryType(state.query) ? state.query : undefined;
|
||||
triggerVisualizeActionsTextBasedLanguages(columns, selectedDataView, aggregateQuery);
|
||||
triggerVisualizeActionsTextBasedLanguages(
|
||||
getUiActions(),
|
||||
columns,
|
||||
PLUGIN_ID,
|
||||
selectedDataView,
|
||||
aggregateQuery
|
||||
);
|
||||
}, [columns, selectedDataView, state.query]);
|
||||
|
||||
if (!selectedDataView) {
|
||||
|
|
|
@ -190,7 +190,9 @@ describe('discover responsive sidebar', function () {
|
|||
<DiscoverSidebarResponsive {...props} />
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
comp.update();
|
||||
// wait for lazy modules
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
await comp.update();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -6,7 +6,52 @@ This Kibana plugin contains components and services for field list UI (as in fie
|
|||
|
||||
## Components
|
||||
|
||||
* `<FieldStats .../>` - loads and renders stats (Top values, Histogram) for a data view field.
|
||||
* `<FieldStats .../>` - loads and renders stats (Top values, Distribution) for a data view field.
|
||||
|
||||
* `<FieldVisualizeButton .../>` - renders a button to open this field in Lens.
|
||||
|
||||
* `<FieldPopover .../>` - a popover container component for a field.
|
||||
|
||||
* `<FieldPopoverHeader .../>` - this header component included a field name and common actions.
|
||||
*
|
||||
* `<FieldPopoverVisualize .../>` - renders Visualize action in the popover footer.
|
||||
|
||||
These components can be combined and customized as the following:
|
||||
```
|
||||
<FieldPopover
|
||||
isOpen={isOpen}
|
||||
closePopover={closePopover}
|
||||
button={<your trigger>}
|
||||
renderHeader={() =>
|
||||
<FieldPopoverHeader
|
||||
field={field}
|
||||
closePopover={closePopover}
|
||||
onAddFieldToWorkspace={onAddFieldToWorkspace}
|
||||
onAddFilter={onAddFilter}
|
||||
onEditField={onEditField}
|
||||
onDeleteField={onDeleteField}
|
||||
...
|
||||
/>
|
||||
}
|
||||
renderContent={() =>
|
||||
<>
|
||||
<FieldStats
|
||||
field={field}
|
||||
dataViewOrDataViewId={dataView}
|
||||
onAddFilter={onAddFilter}
|
||||
...
|
||||
/>
|
||||
<FieldPopoverVisualize
|
||||
field={field}
|
||||
datatView={dataView}
|
||||
originatingApp={'<your app name>'}
|
||||
...
|
||||
/>
|
||||
</>
|
||||
}
|
||||
...
|
||||
/>
|
||||
```
|
||||
|
||||
## Public Services
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
"description": "Contains functionality for the field list which can be integrated into apps",
|
||||
"server": true,
|
||||
"ui": true,
|
||||
"requiredPlugins": ["dataViews", "data", "fieldFormats", "charts"],
|
||||
"requiredPlugins": ["dataViews", "data", "fieldFormats", "charts", "uiActions"],
|
||||
"optionalPlugins": [],
|
||||
"requiredBundles": []
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
.unifiedFieldList__fieldPopover__fieldPopoverPanel {
|
||||
min-width: $euiSizeXXL * 6.5;
|
||||
max-width: $euiSizeXXL * 7.5;
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* 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 { EuiButton, EuiText, EuiPopoverTitle, EuiPopoverFooter } from '@elastic/eui';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { stubLogstashDataView as dataView } from '@kbn/data-views-plugin/common/data_view.stub';
|
||||
import { FieldPopover } from './field_popover';
|
||||
import { FieldPopoverHeader } from './field_popover_header';
|
||||
|
||||
describe('UnifiedFieldList <FieldPopover />', () => {
|
||||
it('should render correctly header only', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<FieldPopover
|
||||
isOpen
|
||||
closePopover={jest.fn()}
|
||||
button={<EuiButton title="test" />}
|
||||
renderHeader={() => <EuiText>{'header'}</EuiText>}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find(EuiText).text()).toBe('header');
|
||||
expect(wrapper.find(EuiPopoverTitle)).toHaveLength(0);
|
||||
expect(wrapper.find(EuiPopoverFooter)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should render correctly with header and content', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<FieldPopover
|
||||
isOpen
|
||||
closePopover={jest.fn()}
|
||||
button={<EuiButton title="test" />}
|
||||
renderHeader={() => <EuiText>{'header'}</EuiText>}
|
||||
renderContent={() => <EuiText>{'content'}</EuiText>}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find(EuiText).first().text()).toBe('header');
|
||||
expect(wrapper.find(EuiText).last().text()).toBe('content');
|
||||
expect(wrapper.find(EuiPopoverTitle)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should render nothing if popover is closed', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<FieldPopover
|
||||
isOpen={false}
|
||||
closePopover={jest.fn()}
|
||||
button={<EuiButton title="test" />}
|
||||
renderHeader={() => <EuiText>{'header'}</EuiText>}
|
||||
renderContent={() => <EuiText>{'content'}</EuiText>}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.text()).toBe('');
|
||||
expect(wrapper.find(EuiPopoverTitle)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should render correctly with popover header and content', async () => {
|
||||
const mockClose = jest.fn();
|
||||
const mockEdit = jest.fn();
|
||||
const fieldName = 'extension';
|
||||
const wrapper = mountWithIntl(
|
||||
<FieldPopover
|
||||
isOpen
|
||||
closePopover={jest.fn()}
|
||||
button={<EuiButton title="test" />}
|
||||
renderHeader={() => (
|
||||
<FieldPopoverHeader
|
||||
field={dataView.fields.find((field) => field.name === fieldName)!}
|
||||
closePopover={mockClose}
|
||||
onEditField={mockEdit}
|
||||
/>
|
||||
)}
|
||||
renderContent={() => <EuiText>{'content'}</EuiText>}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find(EuiPopoverTitle).text()).toBe(fieldName);
|
||||
expect(wrapper.find(EuiText).last().text()).toBe('content');
|
||||
|
||||
wrapper
|
||||
.find(`[data-test-subj="fieldPopoverHeader_editField-${fieldName}"]`)
|
||||
.first()
|
||||
.simulate('click');
|
||||
|
||||
expect(mockClose).toHaveBeenCalled();
|
||||
expect(mockEdit).toHaveBeenCalledWith(fieldName);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 { EuiPopover, EuiPopoverProps, EuiPopoverTitle } from '@elastic/eui';
|
||||
import './field_popover.scss';
|
||||
|
||||
export interface FieldPopoverProps extends EuiPopoverProps {
|
||||
renderHeader?: () => React.ReactNode;
|
||||
renderContent?: () => React.ReactNode;
|
||||
}
|
||||
|
||||
export const FieldPopover: React.FC<FieldPopoverProps> = ({
|
||||
isOpen,
|
||||
closePopover,
|
||||
renderHeader,
|
||||
renderContent,
|
||||
...otherPopoverProps
|
||||
}) => {
|
||||
const header = (isOpen && renderHeader?.()) || null;
|
||||
const content = (isOpen && renderContent?.()) || null;
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
ownFocus
|
||||
isOpen={isOpen}
|
||||
closePopover={closePopover}
|
||||
display="block"
|
||||
anchorPosition="rightUp"
|
||||
data-test-subj="fieldPopover"
|
||||
panelClassName="unifiedFieldList__fieldPopover__fieldPopoverPanel"
|
||||
{...otherPopoverProps}
|
||||
>
|
||||
{isOpen && (
|
||||
<>
|
||||
{content && header ? <EuiPopoverTitle>{header}</EuiPopoverTitle> : header}
|
||||
{content}
|
||||
</>
|
||||
)}
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
* 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 { EuiButtonIcon } from '@elastic/eui';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { stubLogstashDataView as dataView } from '@kbn/data-views-plugin/common/data_view.stub';
|
||||
import { FieldPopoverHeader } from './field_popover_header';
|
||||
|
||||
describe('UnifiedFieldList <FieldPopoverHeader />', () => {
|
||||
it('should render correctly without actions', async () => {
|
||||
const mockClose = jest.fn();
|
||||
const fieldName = 'extension';
|
||||
const wrapper = mountWithIntl(
|
||||
<FieldPopoverHeader
|
||||
field={dataView.fields.find((field) => field.name === fieldName)!}
|
||||
closePopover={mockClose}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.text()).toBe(fieldName);
|
||||
expect(wrapper.find(EuiButtonIcon)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should render correctly with all actions', async () => {
|
||||
const mockClose = jest.fn();
|
||||
const fieldName = 'extension';
|
||||
const field = dataView.fields.find((f) => f.name === fieldName)!;
|
||||
jest.spyOn(field, 'isRuntimeField', 'get').mockImplementation(() => true);
|
||||
const wrapper = mountWithIntl(
|
||||
<FieldPopoverHeader
|
||||
field={field}
|
||||
closePopover={mockClose}
|
||||
onAddFieldToWorkspace={jest.fn()}
|
||||
onAddFilter={jest.fn()}
|
||||
onEditField={jest.fn()}
|
||||
onDeleteField={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.text()).toBe(fieldName);
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="fieldPopoverHeader_addField-${fieldName}"]`).exists()
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="fieldPopoverHeader_addExistsFilter-${fieldName}"]`).exists()
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="fieldPopoverHeader_editField-${fieldName}"]`).exists()
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="fieldPopoverHeader_deleteField-${fieldName}"]`).exists()
|
||||
).toBeTruthy();
|
||||
expect(wrapper.find(EuiButtonIcon)).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('should correctly handle add-field action', async () => {
|
||||
const mockClose = jest.fn();
|
||||
const mockAddField = jest.fn();
|
||||
const fieldName = 'extension';
|
||||
const field = dataView.fields.find((f) => f.name === fieldName)!;
|
||||
const wrapper = mountWithIntl(
|
||||
<FieldPopoverHeader
|
||||
field={field}
|
||||
closePopover={mockClose}
|
||||
onAddFieldToWorkspace={mockAddField}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper
|
||||
.find(`[data-test-subj="fieldPopoverHeader_addField-${fieldName}"]`)
|
||||
.first()
|
||||
.simulate('click');
|
||||
|
||||
expect(mockClose).toHaveBeenCalled();
|
||||
expect(mockAddField).toHaveBeenCalledWith(field);
|
||||
});
|
||||
|
||||
it('should correctly handle add-exists-filter action', async () => {
|
||||
const mockClose = jest.fn();
|
||||
const mockAddFilter = jest.fn();
|
||||
const fieldName = 'extension';
|
||||
const field = dataView.fields.find((f) => f.name === fieldName)!;
|
||||
|
||||
// available
|
||||
let wrapper = mountWithIntl(
|
||||
<FieldPopoverHeader field={field} closePopover={mockClose} onAddFilter={mockAddFilter} />
|
||||
);
|
||||
wrapper
|
||||
.find(`[data-test-subj="fieldPopoverHeader_addExistsFilter-${fieldName}"]`)
|
||||
.first()
|
||||
.simulate('click');
|
||||
expect(mockClose).toHaveBeenCalled();
|
||||
expect(mockAddFilter).toHaveBeenCalledWith('_exists_', fieldName, '+');
|
||||
|
||||
// hidden
|
||||
jest.spyOn(field, 'filterable', 'get').mockImplementation(() => false);
|
||||
wrapper = mountWithIntl(
|
||||
<FieldPopoverHeader field={field} closePopover={mockClose} onAddFilter={mockAddFilter} />
|
||||
);
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="fieldPopoverHeader_addExistsFilter-${fieldName}"]`).exists()
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should correctly handle edit-field action', async () => {
|
||||
const mockClose = jest.fn();
|
||||
const mockEditField = jest.fn();
|
||||
const fieldName = 'extension';
|
||||
const field = dataView.fields.find((f) => f.name === fieldName)!;
|
||||
|
||||
// available
|
||||
jest.spyOn(field, 'isRuntimeField', 'get').mockImplementation(() => true);
|
||||
let wrapper = mountWithIntl(
|
||||
<FieldPopoverHeader field={field} closePopover={mockClose} onEditField={mockEditField} />
|
||||
);
|
||||
wrapper
|
||||
.find(`[data-test-subj="fieldPopoverHeader_editField-${fieldName}"]`)
|
||||
.first()
|
||||
.simulate('click');
|
||||
expect(mockClose).toHaveBeenCalled();
|
||||
expect(mockEditField).toHaveBeenCalledWith(fieldName);
|
||||
|
||||
// hidden
|
||||
jest.spyOn(field, 'isRuntimeField', 'get').mockImplementation(() => false);
|
||||
jest.spyOn(field, 'type', 'get').mockImplementation(() => 'unknown');
|
||||
wrapper = mountWithIntl(
|
||||
<FieldPopoverHeader field={field} closePopover={mockClose} onEditField={mockEditField} />
|
||||
);
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="fieldPopoverHeader_editField-${fieldName}"]`).exists()
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should correctly handle delete-field action', async () => {
|
||||
const mockClose = jest.fn();
|
||||
const mockDeleteField = jest.fn();
|
||||
const fieldName = 'extension';
|
||||
const field = dataView.fields.find((f) => f.name === fieldName)!;
|
||||
|
||||
// available
|
||||
jest.spyOn(field, 'isRuntimeField', 'get').mockImplementation(() => true);
|
||||
let wrapper = mountWithIntl(
|
||||
<FieldPopoverHeader field={field} closePopover={mockClose} onDeleteField={mockDeleteField} />
|
||||
);
|
||||
wrapper
|
||||
.find(`[data-test-subj="fieldPopoverHeader_deleteField-${fieldName}"]`)
|
||||
.first()
|
||||
.simulate('click');
|
||||
expect(mockClose).toHaveBeenCalled();
|
||||
expect(mockDeleteField).toHaveBeenCalledWith(fieldName);
|
||||
|
||||
// hidden
|
||||
jest.spyOn(field, 'isRuntimeField', 'get').mockImplementation(() => false);
|
||||
wrapper = mountWithIntl(
|
||||
<FieldPopoverHeader field={field} closePopover={mockClose} onDeleteField={mockDeleteField} />
|
||||
);
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="fieldPopoverHeader_deleteField-${fieldName}"]`).exists()
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButtonIcon,
|
||||
EuiButtonIconProps,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPopoverProps,
|
||||
EuiToolTip,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { DataViewField } from '@kbn/data-views-plugin/common';
|
||||
import type { AddFieldFilterHandler } from '../../types';
|
||||
|
||||
export interface FieldPopoverHeaderProps {
|
||||
field: DataViewField;
|
||||
closePopover: EuiPopoverProps['closePopover'];
|
||||
buttonAddFieldToWorkspaceProps?: Partial<EuiButtonIconProps>;
|
||||
buttonAddFilterProps?: Partial<EuiButtonIconProps>;
|
||||
buttonEditFieldProps?: Partial<EuiButtonIconProps>;
|
||||
buttonDeleteFieldProps?: Partial<EuiButtonIconProps>;
|
||||
onAddFieldToWorkspace?: (field: DataViewField) => unknown;
|
||||
onAddFilter?: AddFieldFilterHandler;
|
||||
onEditField?: (fieldName: string) => unknown;
|
||||
onDeleteField?: (fieldName: string) => unknown;
|
||||
}
|
||||
|
||||
export const FieldPopoverHeader: React.FC<FieldPopoverHeaderProps> = ({
|
||||
field,
|
||||
closePopover,
|
||||
buttonAddFieldToWorkspaceProps,
|
||||
buttonAddFilterProps,
|
||||
buttonEditFieldProps,
|
||||
buttonDeleteFieldProps,
|
||||
onAddFieldToWorkspace,
|
||||
onAddFilter,
|
||||
onEditField,
|
||||
onDeleteField,
|
||||
}) => {
|
||||
if (!field) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const addFieldToWorkspaceTooltip = i18n.translate(
|
||||
'unifiedFieldList.fieldPopover.addFieldToWorkspaceLabel',
|
||||
{
|
||||
defaultMessage: 'Add "{field}" field',
|
||||
values: {
|
||||
field: field.displayName,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const addExistsFilterTooltip = i18n.translate(
|
||||
'unifiedFieldList.fieldPopover.addExistsFilterLabel',
|
||||
{
|
||||
defaultMessage: 'Filter for field present',
|
||||
}
|
||||
);
|
||||
|
||||
const editFieldTooltip = i18n.translate('unifiedFieldList.fieldPopover.editFieldLabel', {
|
||||
defaultMessage: 'Edit data view field',
|
||||
});
|
||||
|
||||
const deleteFieldTooltip = i18n.translate('unifiedFieldList.fieldPopover.deleteFieldLabel', {
|
||||
defaultMessage: 'Delete data view field',
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiTitle size="xxs">
|
||||
<h5 className="eui-textBreakWord">{field.displayName}</h5>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
{onAddFieldToWorkspace && (
|
||||
<EuiFlexItem grow={false} data-test-subj="fieldPopoverHeader_addField">
|
||||
<EuiToolTip
|
||||
content={buttonAddFieldToWorkspaceProps?.['aria-label'] ?? addFieldToWorkspaceTooltip}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
data-test-subj={`fieldPopoverHeader_addField-${field.name}`}
|
||||
aria-label={addFieldToWorkspaceTooltip}
|
||||
{...(buttonAddFieldToWorkspaceProps || {})}
|
||||
iconType="plusInCircle"
|
||||
onClick={() => {
|
||||
closePopover();
|
||||
onAddFieldToWorkspace(field);
|
||||
}}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{onAddFilter && field.filterable && !field.scripted && (
|
||||
<EuiFlexItem grow={false} data-test-subj="fieldPopoverHeader_addExistsFilter">
|
||||
<EuiToolTip content={buttonAddFilterProps?.['aria-label'] ?? addExistsFilterTooltip}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj={`fieldPopoverHeader_addExistsFilter-${field.name}`}
|
||||
aria-label={addExistsFilterTooltip}
|
||||
{...(buttonAddFilterProps || {})}
|
||||
iconType="filter"
|
||||
onClick={() => {
|
||||
closePopover();
|
||||
onAddFilter('_exists_', field.name, '+');
|
||||
}}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{onEditField &&
|
||||
(field.isRuntimeField || !['unknown', 'unknown_selected'].includes(field.type)) && (
|
||||
<EuiFlexItem grow={false} data-test-subj="fieldPopoverHeader_editField">
|
||||
<EuiToolTip content={buttonEditFieldProps?.['aria-label'] ?? editFieldTooltip}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj={`fieldPopoverHeader_editField-${field.name}`}
|
||||
aria-label={editFieldTooltip}
|
||||
{...(buttonEditFieldProps || {})}
|
||||
iconType="pencil"
|
||||
onClick={() => {
|
||||
closePopover();
|
||||
onEditField(field.name);
|
||||
}}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{onDeleteField && field.isRuntimeField && (
|
||||
<EuiFlexItem grow={false} data-test-subj="fieldPopoverHeader_deleteField">
|
||||
<EuiToolTip content={buttonDeleteFieldProps?.['aria-label'] ?? deleteFieldTooltip}>
|
||||
<EuiButtonIcon
|
||||
data-test-subj={`fieldPopoverHeader_deleteField-${field.name}`}
|
||||
aria-label={deleteFieldTooltip}
|
||||
{...(buttonDeleteFieldProps || {})}
|
||||
color="danger"
|
||||
iconType="trash"
|
||||
onClick={() => {
|
||||
closePopover();
|
||||
onDeleteField(field.name);
|
||||
}}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 { EuiPopoverFooter } from '@elastic/eui';
|
||||
import { FieldVisualizeButton, type FieldVisualizeButtonProps } from '../field_visualize_button';
|
||||
|
||||
export type FieldPopoverVisualizeProps = Omit<FieldVisualizeButtonProps, 'wrapInContainer'>;
|
||||
|
||||
const wrapInContainer = (element: React.ReactElement): React.ReactElement => {
|
||||
return <EuiPopoverFooter>{element}</EuiPopoverFooter>;
|
||||
};
|
||||
|
||||
export const FieldPopoverVisualize: React.FC<FieldPopoverVisualizeProps> = (props) => {
|
||||
return <FieldVisualizeButton {...props} wrapInContainer={wrapInContainer} />;
|
||||
};
|
11
src/plugins/unified_field_list/public/components/field_popover/index.tsx
Executable file
11
src/plugins/unified_field_list/public/components/field_popover/index.tsx
Executable file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 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.
|
||||
*/
|
||||
|
||||
export { type FieldPopoverProps, FieldPopover } from './field_popover';
|
||||
export { type FieldPopoverHeaderProps, FieldPopoverHeader } from './field_popover_header';
|
||||
export { type FieldPopoverVisualizeProps, FieldPopoverVisualize } from './field_popover_visualize';
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* 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 { act } from 'react-dom/test-utils';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import { EuiButton, EuiPopoverFooter } from '@elastic/eui';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { stubLogstashDataView as dataView } from '@kbn/data-views-plugin/common/data_view.stub';
|
||||
import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks';
|
||||
import { FieldVisualizeButton } from './field_visualize_button';
|
||||
import {
|
||||
ACTION_VISUALIZE_LENS_FIELD,
|
||||
VISUALIZE_FIELD_TRIGGER,
|
||||
VISUALIZE_GEO_FIELD_TRIGGER,
|
||||
createAction,
|
||||
VisualizeFieldContext,
|
||||
} from '@kbn/ui-actions-plugin/public';
|
||||
import { TriggerContract } from '@kbn/ui-actions-plugin/public/triggers';
|
||||
|
||||
const ORIGINATING_APP = 'test';
|
||||
const mockExecuteAction = jest.fn();
|
||||
const uiActions = uiActionsPluginMock.createStartContract();
|
||||
const visualizeAction = createAction<VisualizeFieldContext>({
|
||||
type: ACTION_VISUALIZE_LENS_FIELD,
|
||||
id: ACTION_VISUALIZE_LENS_FIELD,
|
||||
getDisplayName: () => 'test',
|
||||
isCompatible: async () => true,
|
||||
execute: async (context: VisualizeFieldContext) => {
|
||||
mockExecuteAction(context);
|
||||
},
|
||||
getHref: async () => '/app/test',
|
||||
});
|
||||
|
||||
jest.spyOn(uiActions, 'getTriggerCompatibleActions').mockResolvedValue([visualizeAction]);
|
||||
jest.spyOn(uiActions, 'getTrigger').mockReturnValue({
|
||||
id: ACTION_VISUALIZE_LENS_FIELD,
|
||||
exec: mockExecuteAction,
|
||||
} as unknown as TriggerContract<object>);
|
||||
|
||||
describe('UnifiedFieldList <FieldVisualizeButton />', () => {
|
||||
it('should render correctly', async () => {
|
||||
const fieldName = 'extension';
|
||||
const field = dataView.fields.find((f) => f.name === fieldName)!;
|
||||
const fieldNameKeyword = 'extension.keyword';
|
||||
const fieldKeyword = dataView.fields.find((f) => f.name === fieldNameKeyword)!;
|
||||
const contextualFields = ['bytes'];
|
||||
jest.spyOn(field, 'visualizable', 'get').mockImplementationOnce(() => false);
|
||||
jest.spyOn(fieldKeyword, 'visualizable', 'get').mockImplementationOnce(() => true);
|
||||
let wrapper: ReactWrapper;
|
||||
|
||||
await act(async () => {
|
||||
wrapper = await mountWithIntl(
|
||||
<FieldVisualizeButton
|
||||
field={field}
|
||||
dataView={dataView}
|
||||
multiFields={[fieldKeyword]}
|
||||
contextualFields={contextualFields}
|
||||
originatingApp={ORIGINATING_APP}
|
||||
uiActions={uiActions}
|
||||
wrapInContainer={(element) => <EuiPopoverFooter>{element}</EuiPopoverFooter>}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
await wrapper!.update();
|
||||
|
||||
expect(uiActions.getTriggerCompatibleActions).toHaveBeenCalledWith(VISUALIZE_FIELD_TRIGGER, {
|
||||
contextualFields,
|
||||
dataViewSpec: dataView.toSpec(false),
|
||||
fieldName: fieldNameKeyword,
|
||||
});
|
||||
|
||||
expect(wrapper!.text()).toBe('Visualize');
|
||||
wrapper!.find(EuiButton).simulate('click');
|
||||
|
||||
expect(mockExecuteAction).toHaveBeenCalledWith({
|
||||
contextualFields,
|
||||
dataViewSpec: dataView.toSpec(false),
|
||||
fieldName: fieldNameKeyword,
|
||||
originatingApp: ORIGINATING_APP,
|
||||
});
|
||||
|
||||
expect(wrapper!.find(EuiButton).prop('href')).toBe('/app/test');
|
||||
expect(wrapper!.find(EuiPopoverFooter).find(EuiButton).exists()).toBeTruthy(); // wrapped in a container
|
||||
});
|
||||
|
||||
it('should render correctly for geo fields', async () => {
|
||||
const fieldName = 'geo.coordinates';
|
||||
const field = dataView.fields.find((f) => f.name === fieldName)!;
|
||||
jest.spyOn(field, 'visualizable', 'get').mockImplementationOnce(() => true);
|
||||
let wrapper: ReactWrapper;
|
||||
|
||||
await act(async () => {
|
||||
wrapper = await mountWithIntl(
|
||||
<FieldVisualizeButton
|
||||
field={field}
|
||||
dataView={dataView}
|
||||
originatingApp={ORIGINATING_APP}
|
||||
uiActions={uiActions}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
await wrapper!.update();
|
||||
|
||||
expect(uiActions.getTriggerCompatibleActions).toHaveBeenCalledWith(
|
||||
VISUALIZE_GEO_FIELD_TRIGGER,
|
||||
{
|
||||
contextualFields: [],
|
||||
dataViewSpec: dataView.toSpec(false),
|
||||
fieldName,
|
||||
}
|
||||
);
|
||||
|
||||
expect(wrapper!.text()).toBe('Visualize');
|
||||
wrapper!.find(EuiButton).simulate('click');
|
||||
|
||||
expect(mockExecuteAction).toHaveBeenCalledWith({
|
||||
contextualFields: [],
|
||||
dataViewSpec: dataView.toSpec(false),
|
||||
fieldName,
|
||||
originatingApp: ORIGINATING_APP,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -7,29 +7,48 @@
|
|||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { EuiButtonProps } from '@elastic/eui';
|
||||
import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics';
|
||||
import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
|
||||
import { triggerVisualizeActions, VisualizeInformation } from './lib/visualize_trigger_utils';
|
||||
import { getVisualizeInformation } from './lib/visualize_trigger_utils';
|
||||
import { DiscoverFieldVisualizeInner } from './discover_field_visualize_inner';
|
||||
import type { UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
import { FieldVisualizeButtonInner } from './field_visualize_button_inner';
|
||||
import {
|
||||
triggerVisualizeActions,
|
||||
getVisualizeInformation,
|
||||
type VisualizeInformation,
|
||||
} from './visualize_trigger_utils';
|
||||
|
||||
interface Props {
|
||||
export interface FieldVisualizeButtonProps {
|
||||
field: DataViewField;
|
||||
dataView: DataView;
|
||||
originatingApp: string; // plugin id
|
||||
uiActions: UiActionsStart;
|
||||
multiFields?: DataViewField[];
|
||||
contextualFields: string[];
|
||||
contextualFields?: string[]; // names of fields which were also selected (like columns in Discover grid)
|
||||
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
|
||||
buttonProps?: Partial<EuiButtonProps>;
|
||||
wrapInContainer?: (element: React.ReactElement) => React.ReactElement;
|
||||
}
|
||||
|
||||
export const DiscoverFieldVisualize: React.FC<Props> = React.memo(
|
||||
({ field, dataView, contextualFields, trackUiMetric, multiFields }) => {
|
||||
export const FieldVisualizeButton: React.FC<FieldVisualizeButtonProps> = React.memo(
|
||||
({
|
||||
field,
|
||||
dataView,
|
||||
contextualFields,
|
||||
trackUiMetric,
|
||||
multiFields,
|
||||
originatingApp,
|
||||
uiActions,
|
||||
buttonProps,
|
||||
wrapInContainer,
|
||||
}) => {
|
||||
const [visualizeInfo, setVisualizeInfo] = useState<VisualizeInformation>();
|
||||
|
||||
useEffect(() => {
|
||||
getVisualizeInformation(field, dataView, contextualFields, multiFields).then(
|
||||
getVisualizeInformation(uiActions, field, dataView, contextualFields, multiFields).then(
|
||||
setVisualizeInfo
|
||||
);
|
||||
}, [contextualFields, field, dataView, multiFields]);
|
||||
}, [contextualFields, field, dataView, multiFields, uiActions]);
|
||||
|
||||
if (!visualizeInfo) {
|
||||
return null;
|
||||
|
@ -43,17 +62,26 @@ export const DiscoverFieldVisualize: React.FC<Props> = React.memo(
|
|||
|
||||
const triggerVisualization = (updatedDataView: DataView) => {
|
||||
trackUiMetric?.(METRIC_TYPE.CLICK, 'visualize_link_click');
|
||||
triggerVisualizeActions(visualizeInfo.field, contextualFields, updatedDataView);
|
||||
triggerVisualizeActions(
|
||||
uiActions,
|
||||
visualizeInfo.field,
|
||||
contextualFields,
|
||||
originatingApp,
|
||||
updatedDataView
|
||||
);
|
||||
};
|
||||
triggerVisualization(dataView);
|
||||
};
|
||||
|
||||
return (
|
||||
<DiscoverFieldVisualizeInner
|
||||
const element = (
|
||||
<FieldVisualizeButtonInner
|
||||
field={field}
|
||||
visualizeInfo={visualizeInfo}
|
||||
handleVisualizeLinkClick={handleVisualizeLinkClick}
|
||||
buttonProps={buttonProps}
|
||||
/>
|
||||
);
|
||||
|
||||
return wrapInContainer?.(element) || element;
|
||||
}
|
||||
);
|
|
@ -7,35 +7,40 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiButton, EuiPopoverFooter } from '@elastic/eui';
|
||||
import { EuiButton, EuiButtonProps } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { DataViewField } from '@kbn/data-views-plugin/public';
|
||||
import { VisualizeInformation } from './lib/visualize_trigger_utils';
|
||||
import { VisualizeInformation } from './visualize_trigger_utils';
|
||||
|
||||
interface DiscoverFieldVisualizeInnerProps {
|
||||
interface FieldVisualizeButtonInnerProps {
|
||||
field: DataViewField;
|
||||
visualizeInfo: VisualizeInformation;
|
||||
handleVisualizeLinkClick: (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void;
|
||||
buttonProps?: Partial<EuiButtonProps>;
|
||||
}
|
||||
|
||||
export const DiscoverFieldVisualizeInner = (props: DiscoverFieldVisualizeInnerProps) => {
|
||||
const { field, visualizeInfo, handleVisualizeLinkClick } = props;
|
||||
|
||||
export const FieldVisualizeButtonInner: React.FC<FieldVisualizeButtonInnerProps> = ({
|
||||
field,
|
||||
visualizeInfo,
|
||||
handleVisualizeLinkClick,
|
||||
buttonProps,
|
||||
}) => {
|
||||
return (
|
||||
<EuiPopoverFooter>
|
||||
<>
|
||||
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
|
||||
<EuiButton
|
||||
fullWidth
|
||||
size="s"
|
||||
data-test-subj={`fieldVisualize-${field.name}`}
|
||||
{...(buttonProps || {})}
|
||||
href={visualizeInfo.href}
|
||||
onClick={handleVisualizeLinkClick}
|
||||
data-test-subj={`fieldVisualize-${field.name}`}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="discover.fieldChooser.visualizeButton.label"
|
||||
id="unifiedFieldList.fieldVisualizeButton.label"
|
||||
defaultMessage="Visualize"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiPopoverFooter>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { type FieldVisualizeButtonProps, FieldVisualizeButton } from './field_visualize_button';
|
||||
|
||||
export {
|
||||
triggerVisualizeActions,
|
||||
triggerVisualizeActionsTextBasedLanguages,
|
||||
getVisualizeInformation,
|
||||
type VisualizeInformation,
|
||||
} from './visualize_trigger_utils';
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import type { DataViewField, DataView } from '@kbn/data-views-plugin/public';
|
||||
import type { Action } from '@kbn/ui-actions-plugin/public';
|
||||
import type { Action, UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
import { getVisualizeInformation } from './visualize_trigger_utils';
|
||||
|
||||
const field = {
|
||||
|
@ -26,11 +26,9 @@ const mockGetActions = jest.fn<Promise<Array<Action<object>>>, [string, { fieldN
|
|||
() => Promise.resolve([])
|
||||
);
|
||||
|
||||
jest.mock('../../../../../kibana_services', () => ({
|
||||
getUiActions: () => ({
|
||||
getTriggerCompatibleActions: mockGetActions,
|
||||
}),
|
||||
}));
|
||||
const uiActions = {
|
||||
getTriggerCompatibleActions: mockGetActions,
|
||||
} as unknown as UiActionsStart;
|
||||
|
||||
const action: Action = {
|
||||
id: 'action',
|
||||
|
@ -51,7 +49,13 @@ describe('visualize_trigger_utils', () => {
|
|||
describe('getVisualizeInformation', () => {
|
||||
it('should return for a visualizeable field with an action', async () => {
|
||||
mockGetActions.mockResolvedValue([action]);
|
||||
const information = await getVisualizeInformation(field, dataViewMock, [], undefined);
|
||||
const information = await getVisualizeInformation(
|
||||
uiActions,
|
||||
field,
|
||||
dataViewMock,
|
||||
[],
|
||||
undefined
|
||||
);
|
||||
expect(information).not.toBeUndefined();
|
||||
expect(information?.field).toHaveProperty('name', 'fieldName');
|
||||
expect(information?.href).toBeUndefined();
|
||||
|
@ -59,7 +63,13 @@ describe('visualize_trigger_utils', () => {
|
|||
|
||||
it('should return field and href from the action', async () => {
|
||||
mockGetActions.mockResolvedValue([{ ...action, getHref: () => Promise.resolve('hreflink') }]);
|
||||
const information = await getVisualizeInformation(field, dataViewMock, [], undefined);
|
||||
const information = await getVisualizeInformation(
|
||||
uiActions,
|
||||
field,
|
||||
dataViewMock,
|
||||
[],
|
||||
undefined
|
||||
);
|
||||
expect(information).not.toBeUndefined();
|
||||
expect(information?.field).toHaveProperty('name', 'fieldName');
|
||||
expect(information).toHaveProperty('href', 'hreflink');
|
||||
|
@ -68,6 +78,7 @@ describe('visualize_trigger_utils', () => {
|
|||
it('should return undefined if no field has a compatible action', async () => {
|
||||
mockGetActions.mockResolvedValue([]);
|
||||
const information = await getVisualizeInformation(
|
||||
uiActions,
|
||||
{ ...field, name: 'rootField' } as DataViewField,
|
||||
dataViewMock,
|
||||
[],
|
||||
|
@ -82,6 +93,7 @@ describe('visualize_trigger_utils', () => {
|
|||
it('should return information for the root field, when multi fields and root are having actions', async () => {
|
||||
mockGetActions.mockResolvedValue([action]);
|
||||
const information = await getVisualizeInformation(
|
||||
uiActions,
|
||||
{ ...field, name: 'rootField' } as DataViewField,
|
||||
dataViewMock,
|
||||
[],
|
||||
|
@ -102,6 +114,7 @@ describe('visualize_trigger_utils', () => {
|
|||
return [];
|
||||
});
|
||||
const information = await getVisualizeInformation(
|
||||
uiActions,
|
||||
{ ...field, name: 'rootField' } as DataViewField,
|
||||
dataViewMock,
|
||||
[],
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
type UiActionsStart,
|
||||
VISUALIZE_FIELD_TRIGGER,
|
||||
VISUALIZE_GEO_FIELD_TRIGGER,
|
||||
visualizeFieldTrigger,
|
||||
|
@ -15,8 +16,6 @@ import {
|
|||
import type { AggregateQuery } from '@kbn/es-query';
|
||||
import type { DataViewField, DataView } from '@kbn/data-views-plugin/public';
|
||||
import { KBN_FIELD_TYPES } from '@kbn/data-plugin/public';
|
||||
import { getUiActions } from '../../../../../kibana_services';
|
||||
import { PLUGIN_ID } from '../../../../../../common';
|
||||
|
||||
export function getTriggerConstant(type: string) {
|
||||
return type === KBN_FIELD_TYPES.GEO_POINT || type === KBN_FIELD_TYPES.GEO_SHAPE
|
||||
|
@ -31,12 +30,13 @@ function getTrigger(type: string) {
|
|||
}
|
||||
|
||||
async function getCompatibleActions(
|
||||
uiActions: UiActionsStart,
|
||||
fieldName: string,
|
||||
dataView: DataView,
|
||||
contextualFields: string[],
|
||||
contextualFields: string[] = [],
|
||||
trigger: typeof VISUALIZE_FIELD_TRIGGER | typeof VISUALIZE_GEO_FIELD_TRIGGER
|
||||
) {
|
||||
const compatibleActions = await getUiActions().getTriggerCompatibleActions(trigger, {
|
||||
const compatibleActions = await uiActions.getTriggerCompatibleActions(trigger, {
|
||||
dataViewSpec: dataView.toSpec(false),
|
||||
fieldName,
|
||||
contextualFields,
|
||||
|
@ -45,8 +45,10 @@ async function getCompatibleActions(
|
|||
}
|
||||
|
||||
export function triggerVisualizeActions(
|
||||
uiActions: UiActionsStart,
|
||||
field: DataViewField,
|
||||
contextualFields: string[],
|
||||
contextualFields: string[] = [],
|
||||
originatingApp: string,
|
||||
dataView?: DataView
|
||||
) {
|
||||
if (!dataView) return;
|
||||
|
@ -55,13 +57,15 @@ export function triggerVisualizeActions(
|
|||
dataViewSpec: dataView.toSpec(false),
|
||||
fieldName: field.name,
|
||||
contextualFields,
|
||||
originatingApp: PLUGIN_ID,
|
||||
originatingApp,
|
||||
};
|
||||
getUiActions().getTrigger(trigger).exec(triggerOptions);
|
||||
uiActions.getTrigger(trigger).exec(triggerOptions);
|
||||
}
|
||||
|
||||
export function triggerVisualizeActionsTextBasedLanguages(
|
||||
uiActions: UiActionsStart,
|
||||
contextualFields: string[],
|
||||
originatingApp: string,
|
||||
dataView?: DataView,
|
||||
query?: AggregateQuery
|
||||
) {
|
||||
|
@ -70,10 +74,10 @@ export function triggerVisualizeActionsTextBasedLanguages(
|
|||
dataViewSpec: dataView.toSpec(false),
|
||||
fieldName: '',
|
||||
contextualFields,
|
||||
originatingApp: PLUGIN_ID,
|
||||
originatingApp,
|
||||
query,
|
||||
};
|
||||
getUiActions().getTrigger(VISUALIZE_FIELD_TRIGGER).exec(triggerOptions);
|
||||
uiActions.getTrigger(VISUALIZE_FIELD_TRIGGER).exec(triggerOptions);
|
||||
}
|
||||
|
||||
export interface VisualizeInformation {
|
||||
|
@ -86,9 +90,10 @@ export interface VisualizeInformation {
|
|||
* that has a compatible visualize uiAction.
|
||||
*/
|
||||
export async function getVisualizeInformation(
|
||||
uiActions: UiActionsStart,
|
||||
field: DataViewField,
|
||||
dataView: DataView | undefined,
|
||||
contextualFields: string[],
|
||||
contextualFields: string[] = [],
|
||||
multiFields: DataViewField[] = []
|
||||
): Promise<VisualizeInformation | undefined> {
|
||||
if (field.name === '_id' || !dataView?.id) {
|
||||
|
@ -102,6 +107,7 @@ export async function getVisualizeInformation(
|
|||
}
|
||||
// Retrieve compatible actions for the specific field
|
||||
const actions = await getCompatibleActions(
|
||||
uiActions,
|
||||
f.name,
|
||||
dataView,
|
||||
contextualFields,
|
||||
|
@ -111,7 +117,7 @@ export async function getVisualizeInformation(
|
|||
// if the field has compatible actions use this field for visualizing
|
||||
if (actions.length > 0) {
|
||||
const triggerOptions = {
|
||||
dataViewSpec: dataView?.toSpec(),
|
||||
dataViewSpec: dataView?.toSpec(false),
|
||||
fieldName: f.name,
|
||||
contextualFields,
|
||||
trigger: getTrigger(f.type),
|
|
@ -16,6 +16,22 @@ export type {
|
|||
} from '../common/types';
|
||||
export type { FieldStatsProps, FieldStatsServices } from './components/field_stats';
|
||||
export { FieldStats } from './components/field_stats';
|
||||
export {
|
||||
FieldPopover,
|
||||
type FieldPopoverProps,
|
||||
FieldPopoverHeader,
|
||||
type FieldPopoverHeaderProps,
|
||||
FieldPopoverVisualize,
|
||||
type FieldPopoverVisualizeProps,
|
||||
} from './components/field_popover';
|
||||
export {
|
||||
FieldVisualizeButton,
|
||||
type FieldVisualizeButtonProps,
|
||||
getVisualizeInformation,
|
||||
triggerVisualizeActions,
|
||||
triggerVisualizeActionsTextBasedLanguages,
|
||||
type VisualizeInformation,
|
||||
} from './components/field_visualize_button';
|
||||
export { loadFieldStats } from './services/field_stats';
|
||||
export { loadFieldExisting } from './services/field_existing';
|
||||
|
||||
|
|
|
@ -14,4 +14,8 @@ export interface UnifiedFieldListPluginSetup {}
|
|||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface UnifiedFieldListPluginStart {}
|
||||
|
||||
export type AddFieldFilterHandler = (field: DataViewField, value: unknown, type: '+' | '-') => void;
|
||||
export type AddFieldFilterHandler = (
|
||||
field: DataViewField | '_exists_',
|
||||
value: unknown,
|
||||
type: '+' | '-'
|
||||
) => void;
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
{ "path": "../kibana_react/tsconfig.json" },
|
||||
{ "path": "../data_views/tsconfig.json" },
|
||||
{ "path": "../data/tsconfig.json" },
|
||||
{ "path": "../charts/tsconfig.json" }
|
||||
{ "path": "../charts/tsconfig.json" },
|
||||
{ "path": "../ui_actions/tsconfig.json" }
|
||||
]
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
expect(await testSubjects.exists('toggleFieldFilterButton')).to.be(true);
|
||||
expect(await testSubjects.exists('fieldTypesHelpButton')).to.be(true);
|
||||
await testSubjects.click('field-@message-showDetails');
|
||||
expect(await testSubjects.exists('discoverFieldListPanelEditItem')).to.be(true);
|
||||
expect(await testSubjects.exists('discoverFieldListPanelEdit-@message')).to.be(true);
|
||||
|
||||
await PageObjects.discover.selectTextBaseLang('SQL');
|
||||
|
||||
|
|
|
@ -47,6 +47,9 @@ export function convertDataViewIntoLensIndexPattern(
|
|||
meta: dataView.metaFields.includes(field.name),
|
||||
esTypes: field.esTypes,
|
||||
scripted: field.scripted,
|
||||
isMapped: field.isMapped,
|
||||
customLabel: field.customLabel,
|
||||
runtimeField: field.runtimeField,
|
||||
runtime: Boolean(field.runtimeField),
|
||||
timeSeriesMetricType: field.timeSeriesMetric,
|
||||
timeSeriesRollup: field.isRolledUpField,
|
||||
|
|
|
@ -9,14 +9,17 @@ import { i18n } from '@kbn/i18n';
|
|||
import { DOCUMENT_FIELD_NAME } from '../../common';
|
||||
import type { IndexPatternField } from '../types';
|
||||
|
||||
const customLabel = i18n.translate('xpack.lens.indexPattern.records', {
|
||||
defaultMessage: 'Records',
|
||||
});
|
||||
|
||||
/**
|
||||
* This is a special-case field which allows us to perform
|
||||
* document-level operations such as count.
|
||||
*/
|
||||
export const documentField: IndexPatternField = {
|
||||
displayName: i18n.translate('xpack.lens.indexPattern.records', {
|
||||
defaultMessage: 'Records',
|
||||
}),
|
||||
displayName: customLabel,
|
||||
customLabel,
|
||||
name: DOCUMENT_FIELD_NAME,
|
||||
type: 'document',
|
||||
aggregatable: true,
|
||||
|
|
|
@ -54,7 +54,3 @@
|
|||
min-width: 260px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.lnsFieldItem__fieldPanelTitle {
|
||||
text-transform: none;
|
||||
}
|
||||
|
|
|
@ -23,9 +23,9 @@ 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';
|
||||
import type { DataView, DataViewField } from '@kbn/data-views-plugin/common';
|
||||
import { DataView, DataViewField } from '@kbn/data-views-plugin/common';
|
||||
import { loadFieldStats } from '@kbn/unified-field-list-plugin/public/services/field_stats';
|
||||
import { FieldStats } from '@kbn/unified-field-list-plugin/public';
|
||||
import { FieldStats, FieldVisualizeButton } from '@kbn/unified-field-list-plugin/public';
|
||||
import { DOCUMENT_FIELD_NAME } from '../../common';
|
||||
import { LensFieldIcon } from '../shared_components';
|
||||
|
||||
|
@ -69,6 +69,14 @@ const InnerFieldItemWrapper: React.FC<FieldItemProps> = (props) => {
|
|||
);
|
||||
};
|
||||
|
||||
async function getComponent(props: FieldItemProps) {
|
||||
const instance = await mountWithIntl(<InnerFieldItemWrapper {...props} />);
|
||||
// wait for lazy modules
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
await instance.update();
|
||||
return instance;
|
||||
}
|
||||
|
||||
describe('IndexPattern Field Item', () => {
|
||||
let defaultProps: FieldItemProps;
|
||||
let indexPattern: IndexPattern;
|
||||
|
@ -122,6 +130,13 @@ describe('IndexPattern Field Item', () => {
|
|||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'geo.coordinates',
|
||||
displayName: 'geo.coordinates',
|
||||
type: 'geo_shape',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
documentField,
|
||||
],
|
||||
} as IndexPattern;
|
||||
|
@ -173,8 +188,8 @@ describe('IndexPattern Field Item', () => {
|
|||
(loadFieldStats as jest.Mock).mockImplementation(() => Promise.resolve({}));
|
||||
});
|
||||
|
||||
it('should display displayName of a field', () => {
|
||||
const wrapper = mountWithIntl(<InnerFieldItemWrapper {...defaultProps} />);
|
||||
it('should display displayName of a field', async () => {
|
||||
const wrapper = await getComponent(defaultProps);
|
||||
|
||||
// Using .toContain over .toEqual because this element includes text from <EuiScreenReaderOnly>
|
||||
// which can't be seen, but shows in the text content
|
||||
|
@ -183,13 +198,11 @@ describe('IndexPattern Field Item', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should show gauge icon for gauge fields', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<InnerFieldItemWrapper
|
||||
{...defaultProps}
|
||||
field={{ ...defaultProps.field, timeSeriesMetricType: 'gauge' }}
|
||||
/>
|
||||
);
|
||||
it('should show gauge icon for gauge fields', async () => {
|
||||
const wrapper = await getComponent({
|
||||
...defaultProps,
|
||||
field: { ...defaultProps.field, timeSeriesMetricType: 'gauge' },
|
||||
});
|
||||
|
||||
// Using .toContain over .toEqual because this element includes text from <EuiScreenReaderOnly>
|
||||
// which can't be seen, but shows in the text content
|
||||
|
@ -198,9 +211,11 @@ describe('IndexPattern Field Item', () => {
|
|||
|
||||
it('should render edit field button if callback is set', async () => {
|
||||
const editFieldSpy = jest.fn();
|
||||
const wrapper = mountWithIntl(
|
||||
<InnerFieldItemWrapper {...defaultProps} editField={editFieldSpy} hideDetails />
|
||||
);
|
||||
const wrapper = await getComponent({
|
||||
...defaultProps,
|
||||
editField: editFieldSpy,
|
||||
hideDetails: true,
|
||||
});
|
||||
await clickField(wrapper, 'bytes');
|
||||
await wrapper.update();
|
||||
const popoverContent = wrapper.find(EuiPopover).prop('children');
|
||||
|
@ -210,7 +225,7 @@ describe('IndexPattern Field Item', () => {
|
|||
{popoverContent as ReactElement}
|
||||
</KibanaContextProvider>
|
||||
)
|
||||
.find('[data-test-subj="lnsFieldListPanelEdit"]')
|
||||
.find('[data-test-subj="fieldPopoverHeader_editField-bytes"]')
|
||||
.first()
|
||||
.simulate('click');
|
||||
});
|
||||
|
@ -219,14 +234,12 @@ describe('IndexPattern Field Item', () => {
|
|||
|
||||
it('should not render edit field button for document field', async () => {
|
||||
const editFieldSpy = jest.fn();
|
||||
const wrapper = mountWithIntl(
|
||||
<InnerFieldItemWrapper
|
||||
{...defaultProps}
|
||||
field={documentField}
|
||||
editField={editFieldSpy}
|
||||
hideDetails
|
||||
/>
|
||||
);
|
||||
const wrapper = await getComponent({
|
||||
...defaultProps,
|
||||
field: documentField,
|
||||
editField: editFieldSpy,
|
||||
hideDetails: true,
|
||||
});
|
||||
await clickField(wrapper, documentField.name);
|
||||
await wrapper.update();
|
||||
const popoverContent = wrapper.find(EuiPopover).prop('children');
|
||||
|
@ -236,12 +249,20 @@ describe('IndexPattern Field Item', () => {
|
|||
{popoverContent as ReactElement}
|
||||
</KibanaContextProvider>
|
||||
)
|
||||
.find('[data-test-subj="lnsFieldListPanelEdit"]')
|
||||
.find('[data-test-subj="fieldPopoverHeader_editField-bytes"]')
|
||||
.exists()
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should pass add filter callback and pass result to filter manager', async () => {
|
||||
let resolveFunction: (arg: unknown) => void;
|
||||
|
||||
(loadFieldStats as jest.Mock).mockImplementation(() => {
|
||||
return new Promise((resolve) => {
|
||||
resolveFunction = resolve;
|
||||
});
|
||||
});
|
||||
|
||||
const field = {
|
||||
name: 'test',
|
||||
displayName: 'testLabel',
|
||||
|
@ -252,25 +273,37 @@ describe('IndexPattern Field Item', () => {
|
|||
};
|
||||
|
||||
const editFieldSpy = jest.fn();
|
||||
const wrapper = mountWithIntl(
|
||||
<InnerFieldItemWrapper {...defaultProps} field={field} editField={editFieldSpy} />
|
||||
);
|
||||
const wrapper = await getComponent({
|
||||
...defaultProps,
|
||||
field,
|
||||
editField: editFieldSpy,
|
||||
});
|
||||
|
||||
await clickField(wrapper, field.name);
|
||||
await wrapper.update();
|
||||
const popoverContent = wrapper.find(EuiPopover).prop('children');
|
||||
const instance = mountWithIntl(
|
||||
<KibanaContextProvider services={mockedServices}>
|
||||
{popoverContent as ReactElement}
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
const onAddFilter = instance.find(FieldStats).prop('onAddFilter');
|
||||
onAddFilter!(field as DataViewField, 'abc', '+');
|
||||
|
||||
await act(async () => {
|
||||
resolveFunction!({
|
||||
totalDocuments: 4633,
|
||||
sampledDocuments: 4633,
|
||||
sampledValues: 4633,
|
||||
topValues: {
|
||||
buckets: [{ count: 147, key: 'abc' }],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await wrapper.update();
|
||||
|
||||
wrapper.find(`[data-test-subj="plus-${field.name}-abc"]`).first().simulate('click');
|
||||
|
||||
expect(mockedServices.data.query.filterManager.addFilters).toHaveBeenCalledWith([
|
||||
expect.objectContaining({ query: { match_phrase: { test: 'abc' } } }),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should request field stats every time the button is clicked', async () => {
|
||||
const dataViewField = new DataViewField(defaultProps.field);
|
||||
let resolveFunction: (arg: unknown) => void;
|
||||
|
||||
(loadFieldStats as jest.Mock).mockImplementation(() => {
|
||||
|
@ -279,7 +312,7 @@ describe('IndexPattern Field Item', () => {
|
|||
});
|
||||
});
|
||||
|
||||
const wrapper = mountWithIntl(<InnerFieldItemWrapper {...defaultProps} />);
|
||||
const wrapper = await getComponent(defaultProps);
|
||||
|
||||
await clickField(wrapper, 'bytes');
|
||||
|
||||
|
@ -299,7 +332,7 @@ describe('IndexPattern Field Item', () => {
|
|||
},
|
||||
fromDate: 'now-7d',
|
||||
toDate: 'now',
|
||||
field: defaultProps.field,
|
||||
field: dataViewField,
|
||||
});
|
||||
|
||||
expect(wrapper.find(EuiPopover).prop('isOpen')).toEqual(true);
|
||||
|
@ -384,14 +417,15 @@ describe('IndexPattern Field Item', () => {
|
|||
},
|
||||
fromDate: 'now-14d',
|
||||
toDate: 'now-7d',
|
||||
field: defaultProps.field,
|
||||
field: dataViewField,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not request field stats for document field', async () => {
|
||||
const wrapper = await mountWithIntl(
|
||||
<InnerFieldItemWrapper {...defaultProps} field={documentField} />
|
||||
);
|
||||
const wrapper = await getComponent({
|
||||
...defaultProps,
|
||||
field: documentField,
|
||||
});
|
||||
|
||||
await clickField(wrapper, DOCUMENT_FIELD_NAME);
|
||||
|
||||
|
@ -404,18 +438,16 @@ describe('IndexPattern Field Item', () => {
|
|||
});
|
||||
|
||||
it('should not request field stats for range fields', async () => {
|
||||
const wrapper = await mountWithIntl(
|
||||
<InnerFieldItemWrapper
|
||||
{...defaultProps}
|
||||
field={{
|
||||
name: 'ip_range',
|
||||
displayName: 'ip_range',
|
||||
type: 'ip_range',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
const wrapper = await getComponent({
|
||||
...defaultProps,
|
||||
field: {
|
||||
name: 'ip_range',
|
||||
displayName: 'ip_range',
|
||||
type: 'ip_range',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
});
|
||||
|
||||
await clickField(wrapper, 'ip_range');
|
||||
|
||||
|
@ -425,6 +457,30 @@ describe('IndexPattern Field Item', () => {
|
|||
expect(wrapper.find(EuiPopover).prop('isOpen')).toEqual(true);
|
||||
expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0);
|
||||
expect(wrapper.find(FieldStats).text()).toBe('Analysis is not available for this field.');
|
||||
expect(wrapper.find(FieldVisualizeButton).exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should not request field stats for geo fields but render Visualize button', async () => {
|
||||
const wrapper = await getComponent({
|
||||
...defaultProps,
|
||||
field: {
|
||||
name: 'geo.coordinates',
|
||||
displayName: 'geo.coordinates',
|
||||
type: 'geo_shape',
|
||||
aggregatable: true,
|
||||
searchable: true,
|
||||
},
|
||||
});
|
||||
|
||||
await clickField(wrapper, 'geo.coordinates');
|
||||
|
||||
await wrapper.update();
|
||||
|
||||
expect(loadFieldStats).toHaveBeenCalled();
|
||||
expect(wrapper.find(EuiPopover).prop('isOpen')).toEqual(true);
|
||||
expect(wrapper.find(EuiLoadingSpinner)).toHaveLength(0);
|
||||
expect(wrapper.find(FieldStats).text()).toBe('Analysis is not available for this field.');
|
||||
expect(wrapper.find(FieldVisualizeButton).exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should display Explore in discover button', async () => {
|
||||
|
|
|
@ -8,36 +8,30 @@
|
|||
import './field_item.scss';
|
||||
|
||||
import React, { useCallback, useState, useMemo } from 'react';
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIconTip,
|
||||
EuiPopover,
|
||||
EuiPopoverTitle,
|
||||
EuiPopoverFooter,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
EuiToolTip,
|
||||
EuiButton,
|
||||
} from '@elastic/eui';
|
||||
import { EuiIconTip, EuiText, EuiButton, EuiPopoverFooter } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { FieldButton } from '@kbn/react-field';
|
||||
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
import { EuiHighlight } from '@elastic/eui';
|
||||
import { Filter, Query } from '@kbn/es-query';
|
||||
import { DataViewField } from '@kbn/data-views-plugin/common';
|
||||
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 } from '@kbn/unified-field-list-plugin/public';
|
||||
import {
|
||||
AddFieldFilterHandler,
|
||||
FieldStats,
|
||||
FieldPopover,
|
||||
FieldPopoverHeader,
|
||||
FieldPopoverVisualize,
|
||||
} from '@kbn/unified-field-list-plugin/public';
|
||||
import { generateFilters, getEsQueryConfig } from '@kbn/data-plugin/public';
|
||||
import { DragDrop, DragDropIdentifier } from '../drag_drop';
|
||||
import { DatasourceDataPanelProps, DataType, DraggedField } from '../types';
|
||||
import { APP_ID } from '../../common/constants';
|
||||
import { DragDrop } from '../drag_drop';
|
||||
import { DatasourceDataPanelProps, DataType } from '../types';
|
||||
import { DOCUMENT_FIELD_NAME } from '../../common';
|
||||
import type { IndexPattern, IndexPatternField } from '../types';
|
||||
import { LensFieldIcon } from '../shared_components/field_picker/lens_field_icon';
|
||||
import { VisualizeGeoFieldButton } from './visualize_geo_field_button';
|
||||
import type { LensAppServices } from '../app_plugin/types';
|
||||
import { debouncedComponent } from '../debounced_component';
|
||||
import { getFieldType } from './pure_utils';
|
||||
|
@ -81,50 +75,64 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) {
|
|||
itemIndex,
|
||||
groupIndex,
|
||||
dropOntoWorkspace,
|
||||
hasSuggestionForField,
|
||||
editField,
|
||||
removeField,
|
||||
} = props;
|
||||
|
||||
const dataViewField = useMemo(() => new DataViewField(field), [field]);
|
||||
const services = useKibana<LensAppServices>().services;
|
||||
const filterManager = services?.data?.query?.filterManager;
|
||||
const [infoIsOpen, setOpen] = useState(false);
|
||||
|
||||
const closeAndEdit = useMemo(
|
||||
const togglePopover = useCallback(() => {
|
||||
setOpen((value) => !value);
|
||||
}, [setOpen]);
|
||||
|
||||
const closePopover = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, [setOpen]);
|
||||
|
||||
const addFilterAndClose: AddFieldFilterHandler | undefined = useMemo(
|
||||
() =>
|
||||
editField
|
||||
? (name: string) => {
|
||||
editField(name);
|
||||
setOpen(false);
|
||||
filterManager
|
||||
? (clickedField, values, operation) => {
|
||||
closePopover();
|
||||
const newFilters = generateFilters(
|
||||
filterManager,
|
||||
clickedField,
|
||||
values,
|
||||
operation,
|
||||
indexPattern
|
||||
);
|
||||
filterManager.addFilters(newFilters);
|
||||
}
|
||||
: undefined,
|
||||
[editField, setOpen]
|
||||
[indexPattern, filterManager, closePopover]
|
||||
);
|
||||
|
||||
const closeAndRemove = useMemo(
|
||||
const editFieldAndClose = useMemo(
|
||||
() =>
|
||||
editField && dataViewField.name !== DOCUMENT_FIELD_NAME
|
||||
? (name: string) => {
|
||||
closePopover();
|
||||
editField(name);
|
||||
}
|
||||
: undefined,
|
||||
[editField, closePopover, dataViewField.name]
|
||||
);
|
||||
|
||||
const removeFieldAndClose = useMemo(
|
||||
() =>
|
||||
removeField
|
||||
? (name: string) => {
|
||||
closePopover();
|
||||
removeField(name);
|
||||
setOpen(false);
|
||||
}
|
||||
: undefined,
|
||||
[removeField, setOpen]
|
||||
[removeField, closePopover]
|
||||
);
|
||||
|
||||
const dropOntoWorkspaceAndClose = useCallback(
|
||||
(droppedField: DragDropIdentifier) => {
|
||||
dropOntoWorkspace(droppedField);
|
||||
setOpen(false);
|
||||
},
|
||||
[dropOntoWorkspace, setOpen]
|
||||
);
|
||||
|
||||
function togglePopover() {
|
||||
setOpen(!infoIsOpen);
|
||||
}
|
||||
|
||||
const onDragStart = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, [setOpen]);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
field,
|
||||
|
@ -137,6 +145,16 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) {
|
|||
}),
|
||||
[field, indexPattern.id, 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 lensFieldIcon = <LensFieldIcon type={getFieldType(field) as DataType} />;
|
||||
|
@ -162,12 +180,15 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) {
|
|||
size="s"
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<li>
|
||||
<EuiPopover
|
||||
ownFocus
|
||||
<FieldPopover
|
||||
isOpen={infoIsOpen}
|
||||
closePopover={closePopover}
|
||||
panelClassName="lnsFieldItem__fieldPanel"
|
||||
initialFocus=".lnsFieldItem__fieldPanel"
|
||||
className="lnsFieldItem__popoverAnchor"
|
||||
display="block"
|
||||
data-test-subj="lnsFieldListPanelField"
|
||||
container={document.querySelector<HTMLElement>('.application') || undefined}
|
||||
button={
|
||||
|
@ -206,152 +227,67 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) {
|
|||
/>
|
||||
</DragDrop>
|
||||
}
|
||||
isOpen={infoIsOpen}
|
||||
closePopover={() => setOpen(false)}
|
||||
anchorPosition="rightUp"
|
||||
panelClassName="lnsFieldItem__fieldPanel"
|
||||
initialFocus=".lnsFieldItem__fieldPanel"
|
||||
>
|
||||
{infoIsOpen && (
|
||||
<FieldItemPopoverContents
|
||||
{...props}
|
||||
editField={closeAndEdit}
|
||||
removeField={closeAndRemove}
|
||||
dropOntoWorkspace={dropOntoWorkspaceAndClose}
|
||||
/>
|
||||
)}
|
||||
</EuiPopover>
|
||||
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.moveToWorkspaceDisabled', {
|
||||
defaultMessage:
|
||||
"This field can't be added to the workspace automatically. You can still use it directly in the configuration panel.",
|
||||
});
|
||||
|
||||
return (
|
||||
<FieldPopoverHeader
|
||||
field={dataViewField}
|
||||
closePopover={closePopover}
|
||||
buttonAddFieldToWorkspaceProps={{
|
||||
isDisabled: !canAddToWorkspace,
|
||||
'aria-label': buttonTitle,
|
||||
}}
|
||||
onAddFieldToWorkspace={dropOntoWorkspaceAndClose}
|
||||
onAddFilter={addFilterAndClose}
|
||||
onEditField={editFieldAndClose}
|
||||
onDeleteField={removeFieldAndClose}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
renderContent={
|
||||
!hideDetails
|
||||
? () => (
|
||||
<FieldItemPopoverContents
|
||||
{...props}
|
||||
dataViewField={dataViewField}
|
||||
onAddFilter={addFilterAndClose}
|
||||
/>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export const FieldItem = debouncedComponent(InnerFieldItem);
|
||||
|
||||
function FieldPanelHeader({
|
||||
indexPatternId,
|
||||
field,
|
||||
hasSuggestionForField,
|
||||
dropOntoWorkspace,
|
||||
editField,
|
||||
removeField,
|
||||
}: {
|
||||
field: IndexPatternField;
|
||||
indexPatternId: string;
|
||||
hasSuggestionForField: DatasourceDataPanelProps['hasSuggestionForField'];
|
||||
dropOntoWorkspace: DatasourceDataPanelProps['dropOntoWorkspace'];
|
||||
editField?: (name: string) => void;
|
||||
removeField?: (name: string) => void;
|
||||
}) {
|
||||
const draggableField = {
|
||||
indexPatternId,
|
||||
id: field.name,
|
||||
field,
|
||||
humanData: {
|
||||
label: field.displayName,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="xxs">
|
||||
<h5 className="eui-textBreakWord lnsFieldItem__fieldPanelTitle">{field.displayName}</h5>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
|
||||
<DragToWorkspaceButton
|
||||
isEnabled={hasSuggestionForField(draggableField)}
|
||||
dropOntoWorkspace={dropOntoWorkspace}
|
||||
field={draggableField}
|
||||
/>
|
||||
{editField && field.name !== DOCUMENT_FIELD_NAME && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content={i18n.translate('xpack.lens.indexPattern.editFieldLabel', {
|
||||
defaultMessage: 'Edit data view field',
|
||||
})}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
onClick={() => editField(field.name)}
|
||||
iconType="pencil"
|
||||
data-test-subj="lnsFieldListPanelEdit"
|
||||
aria-label={i18n.translate('xpack.lens.indexPattern.editFieldLabel', {
|
||||
defaultMessage: 'Edit data view field',
|
||||
})}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{removeField && field.runtime && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content={i18n.translate('xpack.lens.indexPattern.removeFieldLabel', {
|
||||
defaultMessage: 'Remove data view field',
|
||||
})}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
onClick={() => removeField(field.name)}
|
||||
iconType="trash"
|
||||
data-test-subj="lnsFieldListPanelRemove"
|
||||
color="danger"
|
||||
aria-label={i18n.translate('xpack.lens.indexPattern.removeFieldLabel', {
|
||||
defaultMessage: 'Remove data view field',
|
||||
})}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function FieldItemPopoverContents(props: FieldItemProps) {
|
||||
const {
|
||||
query,
|
||||
filters,
|
||||
indexPattern,
|
||||
field,
|
||||
dateRange,
|
||||
dropOntoWorkspace,
|
||||
editField,
|
||||
removeField,
|
||||
hasSuggestionForField,
|
||||
hideDetails,
|
||||
uiActions,
|
||||
core,
|
||||
} = props;
|
||||
function FieldItemPopoverContents(
|
||||
props: FieldItemProps & {
|
||||
dataViewField: DataViewField;
|
||||
onAddFilter: AddFieldFilterHandler | undefined;
|
||||
}
|
||||
) {
|
||||
const { query, filters, indexPattern, dataViewField, dateRange, core, onAddFilter, uiActions } =
|
||||
props;
|
||||
const services = useKibana<LensAppServices>().services;
|
||||
|
||||
const onAddFilter: AddFieldFilterHandler = useCallback(
|
||||
(clickedField, values, operation) => {
|
||||
const filterManager = services.data.query.filterManager;
|
||||
const newFilters = generateFilters(
|
||||
filterManager,
|
||||
clickedField,
|
||||
values,
|
||||
operation,
|
||||
indexPattern
|
||||
);
|
||||
filterManager.addFilters(newFilters);
|
||||
},
|
||||
[indexPattern, services.data.query.filterManager]
|
||||
);
|
||||
|
||||
const panelHeader = (
|
||||
<FieldPanelHeader
|
||||
indexPatternId={indexPattern.id}
|
||||
field={field}
|
||||
dropOntoWorkspace={dropOntoWorkspace}
|
||||
hasSuggestionForField={hasSuggestionForField}
|
||||
editField={editField}
|
||||
removeField={removeField}
|
||||
/>
|
||||
);
|
||||
|
||||
const exploreInDiscover = useMemo(() => {
|
||||
const meta = {
|
||||
id: indexPattern.id,
|
||||
columns: [field.name],
|
||||
columns: [dataViewField.name],
|
||||
filters: {
|
||||
enabled: {
|
||||
lucene: [],
|
||||
|
@ -380,15 +316,10 @@ function FieldItemPopoverContents(props: FieldItemProps) {
|
|||
query: newQuery,
|
||||
columns: meta.columns,
|
||||
});
|
||||
}, [field.name, filters, indexPattern, query, services]);
|
||||
|
||||
if (hideDetails) {
|
||||
return panelHeader;
|
||||
}
|
||||
}, [dataViewField.name, filters, indexPattern, query, services]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPopoverTitle>{panelHeader}</EuiPopoverTitle>
|
||||
<FieldStats
|
||||
services={services}
|
||||
query={query}
|
||||
|
@ -397,25 +328,9 @@ function FieldItemPopoverContents(props: FieldItemProps) {
|
|||
toDate={dateRange.toDate}
|
||||
dataViewOrDataViewId={indexPattern.id} // TODO: Refactor to pass a variable with DataView type instead of IndexPattern
|
||||
onAddFilter={onAddFilter}
|
||||
field={field as DataViewField}
|
||||
field={dataViewField}
|
||||
data-test-subj="lnsFieldListPanel"
|
||||
overrideMissingContent={(params) => {
|
||||
if (field.type === 'geo_point' || field.type === 'geo_shape') {
|
||||
return (
|
||||
<>
|
||||
{params.element}
|
||||
|
||||
<EuiPopoverFooter>
|
||||
<VisualizeGeoFieldButton
|
||||
uiActions={uiActions}
|
||||
indexPattern={indexPattern}
|
||||
fieldName={field.name}
|
||||
/>
|
||||
</EuiPopoverFooter>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (params?.noDataFound) {
|
||||
// TODO: should we replace this with a default message "Analysis is not available for this field?"
|
||||
const isUsingSampling = core.uiSettings.get('lens:useFieldExistenceSampling');
|
||||
|
@ -439,58 +354,32 @@ function FieldItemPopoverContents(props: FieldItemProps) {
|
|||
return params.element;
|
||||
}}
|
||||
/>
|
||||
{exploreInDiscover && field.type !== 'geo_point' && field.type !== 'geo_shape' && (
|
||||
|
||||
{dataViewField.type === 'geo_point' || dataViewField.type === 'geo_shape' ? (
|
||||
<FieldPopoverVisualize
|
||||
field={dataViewField}
|
||||
dataView={{ ...indexPattern, toSpec: () => indexPattern.spec } as unknown as DataView}
|
||||
originatingApp={APP_ID}
|
||||
uiActions={uiActions}
|
||||
buttonProps={{
|
||||
'data-test-subj': `lensVisualize-GeoField-${dataViewField.name}`,
|
||||
}}
|
||||
/>
|
||||
) : exploreInDiscover ? (
|
||||
<EuiPopoverFooter>
|
||||
<EuiButton
|
||||
fullWidth
|
||||
size="s"
|
||||
href={exploreInDiscover}
|
||||
target="_blank"
|
||||
data-test-subj={`lnsFieldListPanel-exploreInDiscover-${field.name}`}
|
||||
data-test-subj={`lnsFieldListPanel-exploreInDiscover-${dataViewField.name}`}
|
||||
>
|
||||
{i18n.translate('xpack.lens.indexPattern.fieldExploreInDiscover', {
|
||||
defaultMessage: 'Explore values in Discover',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiPopoverFooter>
|
||||
)}
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const DragToWorkspaceButton = ({
|
||||
field,
|
||||
dropOntoWorkspace,
|
||||
isEnabled,
|
||||
}: {
|
||||
field: DraggedField;
|
||||
dropOntoWorkspace: DatasourceDataPanelProps['dropOntoWorkspace'];
|
||||
isEnabled: boolean;
|
||||
}) => {
|
||||
const buttonTitle = isEnabled
|
||||
? i18n.translate('xpack.lens.indexPattern.moveToWorkspace', {
|
||||
defaultMessage: 'Add {field} to workspace',
|
||||
values: {
|
||||
field: field.field.name,
|
||||
},
|
||||
})
|
||||
: i18n.translate('xpack.lens.indexPattern.moveToWorkspaceDisabled', {
|
||||
defaultMessage:
|
||||
"This field can't be added to the workspace automatically. You can still use it directly in the configuration panel.",
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip content={buttonTitle}>
|
||||
<EuiButtonIcon
|
||||
aria-label={buttonTitle}
|
||||
isDisabled={!isEnabled}
|
||||
iconType="plusInCircle"
|
||||
onClick={() => {
|
||||
dropOntoWorkspace(field);
|
||||
}}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -50,7 +50,7 @@ function ofName(
|
|||
timeScale: string | undefined,
|
||||
reducedTimeRange: string | undefined
|
||||
) {
|
||||
if (field?.customLabel) {
|
||||
if (field?.customLabel && field?.type !== 'document') {
|
||||
return field.customLabel;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { MouseEvent, useEffect, useState } from 'react';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
visualizeGeoFieldTrigger,
|
||||
VISUALIZE_GEO_FIELD_TRIGGER,
|
||||
UiActionsStart,
|
||||
} from '@kbn/ui-actions-plugin/public';
|
||||
import { APP_ID } from '../../common/constants';
|
||||
import { IndexPattern } from '../types';
|
||||
|
||||
interface Props {
|
||||
indexPattern: IndexPattern;
|
||||
fieldName: string;
|
||||
uiActions: UiActionsStart;
|
||||
}
|
||||
|
||||
export function VisualizeGeoFieldButton(props: Props) {
|
||||
const [href, setHref] = useState<string | undefined>(undefined);
|
||||
|
||||
async function loadHref() {
|
||||
const actions = await props.uiActions.getTriggerCompatibleActions(VISUALIZE_GEO_FIELD_TRIGGER, {
|
||||
dataViewSpec: props.indexPattern.spec,
|
||||
fieldName: props.fieldName,
|
||||
});
|
||||
const triggerOptions = {
|
||||
dataViewSpec: props.indexPattern.spec,
|
||||
fieldName: props.fieldName,
|
||||
trigger: visualizeGeoFieldTrigger,
|
||||
};
|
||||
const loadedHref = actions.length ? await actions[0].getHref?.(triggerOptions) : undefined;
|
||||
setHref(loadedHref);
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
loadHref();
|
||||
},
|
||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||
[]
|
||||
);
|
||||
|
||||
function onClick(event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
props.uiActions.getTrigger(VISUALIZE_GEO_FIELD_TRIGGER).exec({
|
||||
dataViewSpec: props.indexPattern.spec,
|
||||
fieldName: props.fieldName,
|
||||
originatingApp: APP_ID,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
|
||||
<EuiButton
|
||||
fullWidth
|
||||
onClick={onClick}
|
||||
href={href}
|
||||
size="s"
|
||||
data-test-subj={`lensVisualize-GeoField-${props.fieldName}`}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.lens.indexPattern.fieldItem.visualizeGeoFieldButtonText"
|
||||
defaultMessage="Visualize"
|
||||
/>
|
||||
</EuiButton>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -2136,8 +2136,6 @@
|
|||
"discover.fieldChooser.detailViews.emptyStringText": "Chaîne vide",
|
||||
"discover.fieldChooser.discoverField.actions": "Actions",
|
||||
"discover.fieldChooser.discoverField.addFieldTooltip": "Ajouter le champ en tant que colonne",
|
||||
"discover.fieldChooser.discoverField.deleteFieldLabel": "Supprimer le champ de la vue de données",
|
||||
"discover.fieldChooser.discoverField.editFieldLabel": "Modifier le champ de la vue de données",
|
||||
"discover.fieldChooser.discoverField.fieldTopValuesLabel": "Top 5 des valeurs",
|
||||
"discover.fieldChooser.discoverField.multiField": "champ multiple",
|
||||
"discover.fieldChooser.discoverField.multiFields": "Champs multiples",
|
||||
|
@ -2165,7 +2163,6 @@
|
|||
"discover.fieldChooser.searchPlaceHolder": "Rechercher les noms de champs",
|
||||
"discover.fieldChooser.toggleFieldFilterButtonHideAriaLabel": "Masquer les paramètres de filtre de champs",
|
||||
"discover.fieldChooser.toggleFieldFilterButtonShowAriaLabel": "Afficher les paramètres de filtre de champs",
|
||||
"discover.fieldChooser.visualizeButton.label": "Visualiser",
|
||||
"discover.fieldList.flyoutBackIcon": "Retour",
|
||||
"discover.fieldList.flyoutHeading": "Liste des champs",
|
||||
"discover.fieldNameDescription.booleanField": "Valeurs vraies ou fausses.",
|
||||
|
@ -17684,7 +17681,6 @@
|
|||
"xpack.lens.indexPattern.defaultFormatLabel": "Par défaut",
|
||||
"xpack.lens.indexPattern.derivative": "Différences",
|
||||
"xpack.lens.indexPattern.differences.signature": "indicateur : nombre",
|
||||
"xpack.lens.indexPattern.editFieldLabel": "Modifier le champ de la vue de données",
|
||||
"xpack.lens.indexPattern.emptyDimensionButton": "Dimension vide",
|
||||
"xpack.lens.indexPattern.emptyFieldsLabel": "Champs vides",
|
||||
"xpack.lens.indexPattern.emptyFieldsLabelHelp": "Les champs vides ne contenaient aucune valeur dans les 500 premiers documents basés sur vos filtres.",
|
||||
|
@ -17799,7 +17795,6 @@
|
|||
"xpack.lens.indexPattern.ranges.lessThanTooltip": "Inférieur à",
|
||||
"xpack.lens.indexPattern.records": "Enregistrements",
|
||||
"xpack.lens.indexPattern.referenceFunctionPlaceholder": "Sous-fonction",
|
||||
"xpack.lens.indexPattern.removeFieldLabel": "Retirer le champ de la vue de données",
|
||||
"xpack.lens.indexPattern.sortField.invalid": "Champ non valide. Vérifiez votre vue de données ou choisissez un autre champ.",
|
||||
"xpack.lens.indexPattern.standardDeviation": "Écart-type",
|
||||
"xpack.lens.indexPattern.standardDeviation.description": "Agrégation d'indicateurs à valeur unique qui calcule l’écart-type des valeurs numériques extraites des documents agrégés",
|
||||
|
|
|
@ -2132,8 +2132,6 @@
|
|||
"discover.fieldChooser.detailViews.emptyStringText": "空の文字列",
|
||||
"discover.fieldChooser.discoverField.actions": "アクション",
|
||||
"discover.fieldChooser.discoverField.addFieldTooltip": "フィールドを列として追加",
|
||||
"discover.fieldChooser.discoverField.deleteFieldLabel": "データビューフィールドを削除",
|
||||
"discover.fieldChooser.discoverField.editFieldLabel": "データビューフィールドを編集",
|
||||
"discover.fieldChooser.discoverField.fieldTopValuesLabel": "トップ5の値",
|
||||
"discover.fieldChooser.discoverField.multiField": "複数フィールド",
|
||||
"discover.fieldChooser.discoverField.multiFields": "マルチフィールド",
|
||||
|
@ -2161,7 +2159,6 @@
|
|||
"discover.fieldChooser.searchPlaceHolder": "検索フィールド名",
|
||||
"discover.fieldChooser.toggleFieldFilterButtonHideAriaLabel": "フィールド設定を非表示",
|
||||
"discover.fieldChooser.toggleFieldFilterButtonShowAriaLabel": "フィールド設定を表示",
|
||||
"discover.fieldChooser.visualizeButton.label": "可視化",
|
||||
"discover.fieldList.flyoutBackIcon": "戻る",
|
||||
"discover.fieldList.flyoutHeading": "フィールドリスト",
|
||||
"discover.fieldNameDescription.booleanField": "True および False 値。",
|
||||
|
@ -17667,7 +17664,6 @@
|
|||
"xpack.lens.indexPattern.defaultFormatLabel": "デフォルト",
|
||||
"xpack.lens.indexPattern.derivative": "差異",
|
||||
"xpack.lens.indexPattern.differences.signature": "メトリック:数値",
|
||||
"xpack.lens.indexPattern.editFieldLabel": "データビューフィールドを編集",
|
||||
"xpack.lens.indexPattern.emptyDimensionButton": "空のディメンション",
|
||||
"xpack.lens.indexPattern.emptyFieldsLabel": "空のフィールド",
|
||||
"xpack.lens.indexPattern.emptyFieldsLabelHelp": "空のフィールドには、フィルターに基づく最初の 500 件のドキュメントの値が含まれていませんでした。",
|
||||
|
@ -17782,7 +17778,6 @@
|
|||
"xpack.lens.indexPattern.ranges.lessThanTooltip": "より小さい",
|
||||
"xpack.lens.indexPattern.records": "記録",
|
||||
"xpack.lens.indexPattern.referenceFunctionPlaceholder": "サブ関数",
|
||||
"xpack.lens.indexPattern.removeFieldLabel": "データビューフィールドを削除",
|
||||
"xpack.lens.indexPattern.sortField.invalid": "無効なフィールドです。データビューを確認するか、別のフィールドを選択してください。",
|
||||
"xpack.lens.indexPattern.standardDeviation": "標準偏差",
|
||||
"xpack.lens.indexPattern.standardDeviation.description": "集約されたドキュメントから抽出された数値の標準偏差を計算する単一値メトリック集約",
|
||||
|
|
|
@ -2136,8 +2136,6 @@
|
|||
"discover.fieldChooser.detailViews.emptyStringText": "空字符串",
|
||||
"discover.fieldChooser.discoverField.actions": "操作",
|
||||
"discover.fieldChooser.discoverField.addFieldTooltip": "将字段添加为列",
|
||||
"discover.fieldChooser.discoverField.deleteFieldLabel": "删除数据视图字段",
|
||||
"discover.fieldChooser.discoverField.editFieldLabel": "编辑数据视图字段",
|
||||
"discover.fieldChooser.discoverField.fieldTopValuesLabel": "排名前 5 值",
|
||||
"discover.fieldChooser.discoverField.multiField": "多字段",
|
||||
"discover.fieldChooser.discoverField.multiFields": "多字段",
|
||||
|
@ -2165,7 +2163,6 @@
|
|||
"discover.fieldChooser.searchPlaceHolder": "搜索字段名称",
|
||||
"discover.fieldChooser.toggleFieldFilterButtonHideAriaLabel": "隐藏字段筛选设置",
|
||||
"discover.fieldChooser.toggleFieldFilterButtonShowAriaLabel": "显示字段筛选设置",
|
||||
"discover.fieldChooser.visualizeButton.label": "Visualize",
|
||||
"discover.fieldList.flyoutBackIcon": "返回",
|
||||
"discover.fieldList.flyoutHeading": "字段列表",
|
||||
"discover.fieldNameDescription.booleanField": "True 和 False 值。",
|
||||
|
@ -17692,7 +17689,6 @@
|
|||
"xpack.lens.indexPattern.defaultFormatLabel": "默认",
|
||||
"xpack.lens.indexPattern.derivative": "差异",
|
||||
"xpack.lens.indexPattern.differences.signature": "指标:数字",
|
||||
"xpack.lens.indexPattern.editFieldLabel": "编辑数据视图字段",
|
||||
"xpack.lens.indexPattern.emptyDimensionButton": "空维度",
|
||||
"xpack.lens.indexPattern.emptyFieldsLabel": "空字段",
|
||||
"xpack.lens.indexPattern.emptyFieldsLabelHelp": "空字段在基于您的筛选的前 500 个文档中不包含任何值。",
|
||||
|
@ -17807,7 +17803,6 @@
|
|||
"xpack.lens.indexPattern.ranges.lessThanTooltip": "小于",
|
||||
"xpack.lens.indexPattern.records": "记录",
|
||||
"xpack.lens.indexPattern.referenceFunctionPlaceholder": "子函数",
|
||||
"xpack.lens.indexPattern.removeFieldLabel": "移除数据视图字段",
|
||||
"xpack.lens.indexPattern.sortField.invalid": "字段无效。检查数据视图或选取其他字段。",
|
||||
"xpack.lens.indexPattern.standardDeviation": "标准偏差",
|
||||
"xpack.lens.indexPattern.standardDeviation.description": "单值指标聚合,计算从聚合文档提取的数值的标准偏差",
|
||||
|
|
|
@ -102,7 +102,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('should allow removing a field', async () => {
|
||||
await PageObjects.lens.clickField('runtimefield');
|
||||
await PageObjects.lens.removeField();
|
||||
await PageObjects.lens.removeField('runtimefield');
|
||||
await fieldEditor.confirmDelete();
|
||||
await PageObjects.lens.waitForFieldMissing('runtimefield');
|
||||
});
|
||||
|
|
|
@ -50,7 +50,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('should able to edit field', async () => {
|
||||
await PageObjects.lens.clickField('runtimefield');
|
||||
await PageObjects.lens.editField();
|
||||
await PageObjects.lens.editField('runtimefield');
|
||||
await fieldEditor.setName('runtimefield2', true, true);
|
||||
await fieldEditor.save();
|
||||
await fieldEditor.confirmSave();
|
||||
|
@ -69,7 +69,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
it('should able to remove field', async () => {
|
||||
await PageObjects.lens.clickField('runtimefield2');
|
||||
await PageObjects.lens.removeField();
|
||||
await PageObjects.lens.removeField('runtimefield2');
|
||||
await fieldEditor.confirmDelete();
|
||||
await PageObjects.lens.waitForFieldMissing('runtimefield2');
|
||||
});
|
||||
|
|
|
@ -290,17 +290,17 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
|
|||
await testSubjects.click(`lnsFieldListPanelField-${field}`);
|
||||
},
|
||||
|
||||
async editField() {
|
||||
async editField(field: string) {
|
||||
await retry.try(async () => {
|
||||
await testSubjects.click('lnsFieldListPanelEdit');
|
||||
await testSubjects.missingOrFail('lnsFieldListPanelEdit');
|
||||
await testSubjects.click(`fieldPopoverHeader_editField-${field}`);
|
||||
await testSubjects.missingOrFail(`fieldPopoverHeader_editField-${field}`);
|
||||
});
|
||||
},
|
||||
|
||||
async removeField() {
|
||||
async removeField(field: string) {
|
||||
await retry.try(async () => {
|
||||
await testSubjects.click('lnsFieldListPanelRemove');
|
||||
await testSubjects.missingOrFail('lnsFieldListPanelRemove');
|
||||
await testSubjects.click(`fieldPopoverHeader_deleteField-${field}`);
|
||||
await testSubjects.missingOrFail(`fieldPopoverHeader_deleteField-${field}`);
|
||||
});
|
||||
},
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue