[ML] Add KQL filter bar, filtering functionality, and compact design to Index data visualizer (#112870)

* [ML] Initial embed

* [ML] Initial embed props

* [ML] Add top nav link to data viz

* Add visible fields

* Add add data service to register links

* Renames, refactor, use constants

* Renames, refactor, use constants

* Update tests and mocks

* Embeddable

* Update hook to update upon time udpate

* Add filter support to query

* Refactor filter utilities

* Add filter support for embeddable

* Fix saved search data undefined

* Prototype aggregated view/document view switcher

* Prototype flyout

* Prototype save document view option in storage

* Fix filter and query conflict with saved search

* Minor styling edits

* [ML] Update functional tests to reflect new arrow icons

* [ML] Add filter buttons and KQL bars

* [ML] Update filter bar onChange behavior

* [ML] Update top values filter onChange behavior

* [ML] Update search filters when opening saved search

* [ML] Clean up

* [ML] Remove fit content for height

* [ML] Fix boolean legend

* [ML] Fix header section when browser width is small to large and when index pattern title is too large

* [ML] Hide expander icon when dimension is xs or s & css fixes

* [ML] Delete embeddables because they are not use

* [ML] Make doc count 0 for empty fields, update t/f test

* [ML] Add unit testing for search utils

* [ML] No need to - padding

* [ML] Fix expand all/collapse all behavior to override individual setting

* [ML] Fix functional tests should be 0/0%

* [ML] Fix docs content spacing, rename classnames, add filters to Discover, lens, and maps

* [ML] Fix icon styling to match Discover but have text/keyword/histogram

* [ML] Fix filters not persisting after page refresh & on query change

* [ML] Rename classnames to BEM style

* [ML] Fix doc count for fields that exists but have no stats

* [ML] Clean up unused styles

* [ML] Fix eui var & icon & file geo

* [ML] Fix navigating to Lens from new saved search broken

* [ML] Change types back to Index pattern for 7.16

* [ML] Update not in docs content and snapshots

* [ML] Fix Lens and indexRefName

* [ML] Fix field icon and texts not aligned, remove span because EuiToolTip now supports EuiToken

* [ML] Fix data view
This commit is contained in:
Quynh Nguyen 2021-09-30 11:37:56 -05:00 committed by GitHub
parent b73d939d6c
commit 747212ce45
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
75 changed files with 1749 additions and 824 deletions

View file

@ -33,19 +33,6 @@ export const JOB_FIELD_TYPES = {
UNKNOWN: 'unknown', UNKNOWN: 'unknown',
} as const; } as const;
export const JOB_FIELD_TYPES_OPTIONS = {
[JOB_FIELD_TYPES.BOOLEAN]: { name: 'Boolean', icon: 'tokenBoolean' },
[JOB_FIELD_TYPES.DATE]: { name: 'Date', icon: 'tokenDate' },
[JOB_FIELD_TYPES.GEO_POINT]: { name: 'Geo point', icon: 'tokenGeo' },
[JOB_FIELD_TYPES.GEO_SHAPE]: { name: 'Geo shape', icon: 'tokenGeo' },
[JOB_FIELD_TYPES.IP]: { name: 'IP address', icon: 'tokenIP' },
[JOB_FIELD_TYPES.KEYWORD]: { name: 'Keyword', icon: 'tokenKeyword' },
[JOB_FIELD_TYPES.NUMBER]: { name: 'Number', icon: 'tokenNumber' },
[JOB_FIELD_TYPES.TEXT]: { name: 'Text', icon: 'tokenString' },
[JOB_FIELD_TYPES.HISTOGRAM]: { name: 'Histogram', icon: 'tokenNumber' },
[JOB_FIELD_TYPES.UNKNOWN]: { name: 'Unknown' },
};
export const OMIT_FIELDS: string[] = ['_source', '_type', '_index', '_id', '_version', '_score']; export const OMIT_FIELDS: string[] = ['_source', '_type', '_index', '_id', '_version', '_score'];
export const NON_AGGREGATABLE_FIELD_TYPES = new Set<string>([ export const NON_AGGREGATABLE_FIELD_TYPES = new Set<string>([

View file

@ -6,6 +6,7 @@
*/ */
import type { SimpleSavedObject } from 'kibana/public'; import type { SimpleSavedObject } from 'kibana/public';
import { isPopulatedObject } from '../utils/object_utils';
export type { JobFieldType } from './job_field_type'; export type { JobFieldType } from './job_field_type';
export type { export type {
FieldRequestConfig, FieldRequestConfig,
@ -27,3 +28,7 @@ export interface DataVisualizerTableState {
} }
export type SavedSearchSavedObject = SimpleSavedObject<any>; export type SavedSearchSavedObject = SimpleSavedObject<any>;
export function isSavedSearchSavedObject(arg: unknown): arg is SavedSearchSavedObject {
return isPopulatedObject(arg, ['id', 'type', 'attributes']);
}

View file

@ -1,4 +1,4 @@
.embeddedMapContent { .embeddedMap__content {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex; display: flex;

View file

@ -39,7 +39,7 @@ export function EmbeddedMapComponent({
const baseLayers = useRef<LayerDescriptor[]>(); const baseLayers = useRef<LayerDescriptor[]>();
const { const {
services: { embeddable: embeddablePlugin, maps: mapsPlugin }, services: { embeddable: embeddablePlugin, maps: mapsPlugin, data },
} = useDataVisualizerKibana(); } = useDataVisualizerKibana();
const factory: const factory:
@ -73,7 +73,7 @@ export function EmbeddedMapComponent({
const input: MapEmbeddableInput = { const input: MapEmbeddableInput = {
id: htmlIdGenerator()(), id: htmlIdGenerator()(),
attributes: { title: '' }, attributes: { title: '' },
filters: [], filters: data.query.filterManager.getFilters() ?? [],
hidePanelTitles: true, hidePanelTitles: true,
viewMode: ViewMode.VIEW, viewMode: ViewMode.VIEW,
isLayerTOCOpen: false, isLayerTOCOpen: false,
@ -143,7 +143,7 @@ export function EmbeddedMapComponent({
return ( return (
<div <div
data-test-subj="dataVisualizerEmbeddedMapContent" data-test-subj="dataVisualizerEmbeddedMapContent"
className="embeddedMapContent" className="embeddedMap__content"
ref={embeddableRoot} ref={embeddableRoot}
/> />
); );

View file

@ -11,6 +11,7 @@ import { EuiListGroup, EuiListGroupItem } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react'; import { FormattedMessage } from '@kbn/i18n/react';
import { ExpandedRowFieldHeader } from '../stats_table/components/expanded_row_field_header'; import { ExpandedRowFieldHeader } from '../stats_table/components/expanded_row_field_header';
import { ExpandedRowPanel } from '../stats_table/components/field_data_expanded_row/expanded_row_panel';
interface Props { interface Props {
examples: Array<string | object>; examples: Array<string | object>;
} }
@ -31,8 +32,7 @@ export const ExamplesList: FC<Props> = ({ examples }) => {
examplesContent = examples.map((example, i) => { examplesContent = examples.map((example, i) => {
return ( return (
<EuiListGroupItem <EuiListGroupItem
className="fieldDataCard__codeContent" size="xs"
size="s"
key={`example_${i}`} key={`example_${i}`}
label={typeof example === 'string' ? example : JSON.stringify(example)} label={typeof example === 'string' ? example : JSON.stringify(example)}
/> />
@ -41,7 +41,10 @@ export const ExamplesList: FC<Props> = ({ examples }) => {
} }
return ( return (
<div data-test-subj="dataVisualizerFieldDataExamplesList"> <ExpandedRowPanel
dataTestSubj="dataVisualizerFieldDataExamplesList"
className="dvText__wrapper dvPanel__wrapper"
>
<ExpandedRowFieldHeader> <ExpandedRowFieldHeader>
<FormattedMessage <FormattedMessage
id="xpack.dataVisualizer.dataGrid.field.examplesList.title" id="xpack.dataVisualizer.dataGrid.field.examplesList.title"
@ -54,6 +57,6 @@ export const ExamplesList: FC<Props> = ({ examples }) => {
<EuiListGroup showToolTips={true} maxWidth={'s'} gutterSize={'none'} flush={true}> <EuiListGroup showToolTips={true} maxWidth={'s'} gutterSize={'none'} flush={true}>
{examplesContent} {examplesContent}
</EuiListGroup> </EuiListGroup>
</div> </ExpandedRowPanel>
); );
}; };

View file

@ -52,10 +52,7 @@ export const FileBasedDataVisualizerExpandedRow = ({ item }: { item: FileBasedFi
} }
return ( return (
<div <div className="dvExpandedRow" data-test-subj={`dataVisualizerFieldExpandedRow-${fieldName}`}>
className="dataVisualizerFieldExpandedRow"
data-test-subj={`dataVisualizerFieldExpandedRow-${fieldName}`}
>
{getCardContent()} {getCardContent()}
</div> </div>
); );

View file

@ -6,8 +6,6 @@
*/ */
import React, { FC, useMemo } from 'react'; import React, { FC, useMemo } from 'react';
import { EuiFlexItem } from '@elastic/eui';
import { Feature, Point } from 'geojson'; import { Feature, Point } from 'geojson';
import type { FieldDataRowProps } from '../../stats_table/types/field_data_row'; import type { FieldDataRowProps } from '../../stats_table/types/field_data_row';
import { DocumentStatsTable } from '../../stats_table/components/field_data_expanded_row/document_stats'; import { DocumentStatsTable } from '../../stats_table/components/field_data_expanded_row/document_stats';
@ -15,6 +13,7 @@ import { EmbeddedMapComponent } from '../../embedded_map';
import { convertWKTGeoToLonLat, getGeoPointsLayer } from './format_utils'; import { convertWKTGeoToLonLat, getGeoPointsLayer } from './format_utils';
import { ExpandedRowContent } from '../../stats_table/components/field_data_expanded_row/expanded_row_content'; import { ExpandedRowContent } from '../../stats_table/components/field_data_expanded_row/expanded_row_content';
import { ExamplesList } from '../../examples_list'; import { ExamplesList } from '../../examples_list';
import { ExpandedRowPanel } from '../../stats_table/components/field_data_expanded_row/expanded_row_panel';
export const DEFAULT_GEO_REGEX = RegExp('(?<lat>.+) (?<lon>.+)'); export const DEFAULT_GEO_REGEX = RegExp('(?<lat>.+) (?<lon>.+)');
@ -63,17 +62,12 @@ export const GeoPointContent: FC<FieldDataRowProps> = ({ config }) => {
<ExpandedRowContent dataTestSubj={'dataVisualizerGeoPointContent'}> <ExpandedRowContent dataTestSubj={'dataVisualizerGeoPointContent'}>
<DocumentStatsTable config={config} /> <DocumentStatsTable config={config} />
{formattedResults && Array.isArray(formattedResults.examples) && ( {formattedResults && Array.isArray(formattedResults.examples) && (
<EuiFlexItem> <ExamplesList examples={formattedResults.examples} />
<ExamplesList examples={formattedResults.examples} />
</EuiFlexItem>
)} )}
{formattedResults && Array.isArray(formattedResults.layerList) && ( {formattedResults && Array.isArray(formattedResults.layerList) && (
<EuiFlexItem <ExpandedRowPanel className={'dvPanel__wrapper dvMap__wrapper'} grow={true}>
className={'dataVisualizerMapWrapper'}
data-test-subj={'dataVisualizerEmbeddedMap'}
>
<EmbeddedMapComponent layerList={formattedResults.layerList} /> <EmbeddedMapComponent layerList={formattedResults.layerList} />
</EuiFlexItem> </ExpandedRowPanel>
)} )}
</ExpandedRowContent> </ExpandedRowContent>
); );

View file

@ -6,7 +6,6 @@
*/ */
import React, { FC, useEffect, useState } from 'react'; import React, { FC, useEffect, useState } from 'react';
import { EuiFlexItem } from '@elastic/eui';
import { IndexPattern } from '../../../../../../../../../src/plugins/data/common'; import { IndexPattern } from '../../../../../../../../../src/plugins/data/common';
import { CombinedQuery } from '../../../../index_data_visualizer/types/combined_query'; import { CombinedQuery } from '../../../../index_data_visualizer/types/combined_query';
import { ExpandedRowContent } from '../../stats_table/components/field_data_expanded_row/expanded_row_content'; import { ExpandedRowContent } from '../../stats_table/components/field_data_expanded_row/expanded_row_content';
@ -17,6 +16,7 @@ import { useDataVisualizerKibana } from '../../../../kibana_context';
import { JOB_FIELD_TYPES } from '../../../../../../common'; import { JOB_FIELD_TYPES } from '../../../../../../common';
import { ES_GEO_FIELD_TYPE, LayerDescriptor } from '../../../../../../../maps/common'; import { ES_GEO_FIELD_TYPE, LayerDescriptor } from '../../../../../../../maps/common';
import { EmbeddedMapComponent } from '../../embedded_map'; import { EmbeddedMapComponent } from '../../embedded_map';
import { ExpandedRowPanel } from '../../stats_table/components/field_data_expanded_row/expanded_row_panel';
export const GeoPointContentWithMap: FC<{ export const GeoPointContentWithMap: FC<{
config: FieldVisConfig; config: FieldVisConfig;
@ -26,7 +26,7 @@ export const GeoPointContentWithMap: FC<{
const { stats } = config; const { stats } = config;
const [layerList, setLayerList] = useState<LayerDescriptor[]>([]); const [layerList, setLayerList] = useState<LayerDescriptor[]>([]);
const { const {
services: { maps: mapsPlugin }, services: { maps: mapsPlugin, data },
} = useDataVisualizerKibana(); } = useDataVisualizerKibana();
// Update the layer list with updated geo points upon refresh // Update the layer list with updated geo points upon refresh
@ -42,6 +42,7 @@ export const GeoPointContentWithMap: FC<{
indexPatternId: indexPattern.id, indexPatternId: indexPattern.id,
geoFieldName: config.fieldName, geoFieldName: config.fieldName,
geoFieldType: config.type as ES_GEO_FIELD_TYPE, geoFieldType: config.type as ES_GEO_FIELD_TYPE,
filters: data.query.filterManager.getFilters() ?? [],
query: { query: {
query: combinedQuery.searchString, query: combinedQuery.searchString,
language: combinedQuery.searchQueryLanguage, language: combinedQuery.searchQueryLanguage,
@ -57,19 +58,16 @@ export const GeoPointContentWithMap: FC<{
} }
updateIndexPatternSearchLayer(); updateIndexPatternSearchLayer();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [indexPattern, combinedQuery, config, mapsPlugin]); }, [indexPattern, combinedQuery, config, mapsPlugin, data.query]);
if (stats?.examples === undefined) return null; if (stats?.examples === undefined) return null;
return ( return (
<ExpandedRowContent dataTestSubj={'dataVisualizerIndexBasedMapContent'}> <ExpandedRowContent dataTestSubj={'dataVisualizerIndexBasedMapContent'}>
<DocumentStatsTable config={config} /> <DocumentStatsTable config={config} />
<ExamplesList examples={stats.examples} />
<EuiFlexItem style={{ maxWidth: '50%' }}> <ExpandedRowPanel className={'dvPanel__wrapper dvMap__wrapper'} grow={true}>
<ExamplesList examples={stats.examples} />
</EuiFlexItem>
<EuiFlexItem className={'dataVisualizerMapWrapper'}>
<EmbeddedMapComponent layerList={layerList} /> <EmbeddedMapComponent layerList={layerList} />
</EuiFlexItem> </ExpandedRowPanel>
</ExpandedRowContent> </ExpandedRowContent>
); );
}; };

View file

@ -22,15 +22,21 @@ import { FieldVisConfig } from '../stats_table/types';
import { IndexPattern } from '../../../../../../../../src/plugins/data/common'; import { IndexPattern } from '../../../../../../../../src/plugins/data/common';
import { CombinedQuery } from '../../../index_data_visualizer/types/combined_query'; import { CombinedQuery } from '../../../index_data_visualizer/types/combined_query';
import { LoadingIndicator } from '../loading_indicator'; import { LoadingIndicator } from '../loading_indicator';
import { IndexPatternField } from '../../../../../../../../src/plugins/data/common';
export const IndexBasedDataVisualizerExpandedRow = ({ export const IndexBasedDataVisualizerExpandedRow = ({
item, item,
indexPattern, indexPattern,
combinedQuery, combinedQuery,
onAddFilter,
}: { }: {
item: FieldVisConfig; item: FieldVisConfig;
indexPattern: IndexPattern | undefined; indexPattern: IndexPattern | undefined;
combinedQuery: CombinedQuery; combinedQuery: CombinedQuery;
/**
* Callback to add a filter to filter bar
*/
onAddFilter?: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
}) => { }) => {
const config = item; const config = item;
const { loading, type, existsInDocs, fieldName } = config; const { loading, type, existsInDocs, fieldName } = config;
@ -42,7 +48,7 @@ export const IndexBasedDataVisualizerExpandedRow = ({
switch (type) { switch (type) {
case JOB_FIELD_TYPES.NUMBER: case JOB_FIELD_TYPES.NUMBER:
return <NumberContent config={config} />; return <NumberContent config={config} onAddFilter={onAddFilter} />;
case JOB_FIELD_TYPES.BOOLEAN: case JOB_FIELD_TYPES.BOOLEAN:
return <BooleanContent config={config} />; return <BooleanContent config={config} />;
@ -61,10 +67,10 @@ export const IndexBasedDataVisualizerExpandedRow = ({
); );
case JOB_FIELD_TYPES.IP: case JOB_FIELD_TYPES.IP:
return <IpContent config={config} />; return <IpContent config={config} onAddFilter={onAddFilter} />;
case JOB_FIELD_TYPES.KEYWORD: case JOB_FIELD_TYPES.KEYWORD:
return <KeywordContent config={config} />; return <KeywordContent config={config} onAddFilter={onAddFilter} />;
case JOB_FIELD_TYPES.TEXT: case JOB_FIELD_TYPES.TEXT:
return <TextContent config={config} />; return <TextContent config={config} />;
@ -75,10 +81,7 @@ export const IndexBasedDataVisualizerExpandedRow = ({
} }
return ( return (
<div <div className="dvExpandedRow" data-test-subj={`dataVisualizerFieldExpandedRow-${fieldName}`}>
className="dataVisualizerFieldExpandedRow"
data-test-subj={`dataVisualizerFieldExpandedRow-${fieldName}`}
>
{loading === true ? <LoadingIndicator /> : getCardContent()} {loading === true ? <LoadingIndicator /> : getCardContent()}
</div> </div>
); );

View file

@ -28,12 +28,13 @@ export const FieldCountPanel: FC<Props> = ({
<EuiFlexGroup <EuiFlexGroup
alignItems="center" alignItems="center"
gutterSize="xs" gutterSize="xs"
style={{ marginLeft: 4 }}
data-test-subj="dataVisualizerFieldCountPanel" data-test-subj="dataVisualizerFieldCountPanel"
responsive={false}
className="dvFieldCount__panel"
> >
<TotalFieldsCount fieldsCountStats={fieldsCountStats} /> <TotalFieldsCount fieldsCountStats={fieldsCountStats} />
<MetricFieldsCount metricsStats={metricsStats} /> <MetricFieldsCount metricsStats={metricsStats} />
<EuiFlexItem> <EuiFlexItem className={'dvFieldCount__item'}>
<EuiSwitch <EuiSwitch
data-test-subj="dataVisualizerShowEmptyFieldsSwitch" data-test-subj="dataVisualizerShowEmptyFieldsSwitch"
label={ label={

View file

@ -20,13 +20,14 @@ import {
export function getActions( export function getActions(
indexPattern: IndexPattern, indexPattern: IndexPattern,
services: DataVisualizerKibanaReactContextValue['services'], services: Partial<DataVisualizerKibanaReactContextValue['services']>,
combinedQuery: CombinedQuery, combinedQuery: CombinedQuery,
actionFlyoutRef: MutableRefObject<(() => void | undefined) | undefined> actionFlyoutRef: MutableRefObject<(() => void | undefined) | undefined>
): Array<Action<FieldVisConfig>> { ): Array<Action<FieldVisConfig>> {
const { lens: lensPlugin, indexPatternFieldEditor } = services; const { lens: lensPlugin, data } = services;
const actions: Array<Action<FieldVisConfig>> = []; const actions: Array<Action<FieldVisConfig>> = [];
const filters = data?.query.filterManager.getFilters() ?? [];
const refreshPage = () => { const refreshPage = () => {
const refresh: Refresh = { const refresh: Refresh = {
@ -49,7 +50,7 @@ export function getActions(
available: (item: FieldVisConfig) => available: (item: FieldVisConfig) =>
getCompatibleLensDataType(item.type) !== undefined && canUseLensEditor, getCompatibleLensDataType(item.type) !== undefined && canUseLensEditor,
onClick: (item: FieldVisConfig) => { onClick: (item: FieldVisConfig) => {
const lensAttributes = getLensAttributes(indexPattern, combinedQuery, item); const lensAttributes = getLensAttributes(indexPattern, combinedQuery, filters, item);
if (lensAttributes) { if (lensAttributes) {
lensPlugin.navigateToPrefilledEditor({ lensPlugin.navigateToPrefilledEditor({
id: `dataVisualizer-${item.fieldName}`, id: `dataVisualizer-${item.fieldName}`,
@ -62,7 +63,7 @@ export function getActions(
} }
// Allow to edit index pattern field // Allow to edit index pattern field
if (indexPatternFieldEditor?.userPermissions.editIndexPattern()) { if (services.indexPatternFieldEditor?.userPermissions.editIndexPattern()) {
actions.push({ actions.push({
name: i18n.translate('xpack.dataVisualizer.index.dataGrid.editIndexPatternFieldTitle', { name: i18n.translate('xpack.dataVisualizer.index.dataGrid.editIndexPatternFieldTitle', {
defaultMessage: 'Edit index pattern field', defaultMessage: 'Edit index pattern field',
@ -76,7 +77,7 @@ export function getActions(
type: 'icon', type: 'icon',
icon: 'indexEdit', icon: 'indexEdit',
onClick: (item: FieldVisConfig) => { onClick: (item: FieldVisConfig) => {
actionFlyoutRef.current = indexPatternFieldEditor?.openEditor({ actionFlyoutRef.current = services.indexPatternFieldEditor?.openEditor({
ctx: { indexPattern }, ctx: { indexPattern },
fieldName: item.fieldName, fieldName: item.fieldName,
onSave: refreshPage, onSave: refreshPage,
@ -100,7 +101,7 @@ export function getActions(
return item.deletable === true; return item.deletable === true;
}, },
onClick: (item: FieldVisConfig) => { onClick: (item: FieldVisConfig) => {
actionFlyoutRef.current = indexPatternFieldEditor?.openDeleteModal({ actionFlyoutRef.current = services.indexPatternFieldEditor?.openDeleteModal({
ctx: { indexPattern }, ctx: { indexPattern },
fieldName: item.fieldName!, fieldName: item.fieldName!,
onDelete: refreshPage, onDelete: refreshPage,

View file

@ -6,6 +6,7 @@
*/ */
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import type { Filter } from '@kbn/es-query';
import type { IndexPattern } from '../../../../../../../../../src/plugins/data/common'; import type { IndexPattern } from '../../../../../../../../../src/plugins/data/common';
import type { CombinedQuery } from '../../../../index_data_visualizer/types/combined_query'; import type { CombinedQuery } from '../../../../index_data_visualizer/types/combined_query';
import type { import type {
@ -15,6 +16,7 @@ import type {
} from '../../../../../../../lens/public'; } from '../../../../../../../lens/public';
import { FieldVisConfig } from '../../stats_table/types'; import { FieldVisConfig } from '../../stats_table/types';
import { JOB_FIELD_TYPES } from '../../../../../../common'; import { JOB_FIELD_TYPES } from '../../../../../../common';
interface ColumnsAndLayer { interface ColumnsAndLayer {
columns: Record<string, IndexPatternColumn>; columns: Record<string, IndexPatternColumn>;
layer: XYLayerConfig; layer: XYLayerConfig;
@ -241,6 +243,7 @@ function getColumnsAndLayer(
export function getLensAttributes( export function getLensAttributes(
defaultIndexPattern: IndexPattern | undefined, defaultIndexPattern: IndexPattern | undefined,
combinedQuery: CombinedQuery, combinedQuery: CombinedQuery,
filters: Filter[],
item: FieldVisConfig item: FieldVisConfig
): TypedLensByValueInput['attributes'] | undefined { ): TypedLensByValueInput['attributes'] | undefined {
if (defaultIndexPattern === undefined || item.type === undefined || item.fieldName === undefined) if (defaultIndexPattern === undefined || item.type === undefined || item.fieldName === undefined)
@ -279,7 +282,7 @@ export function getLensAttributes(
}, },
}, },
}, },
filters: [], filters,
query: { language: combinedQuery.searchQueryLanguage, query: combinedQuery.searchString }, query: { language: combinedQuery.searchQueryLanguage, query: combinedQuery.searchString },
visualization: { visualization: {
axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true },

View file

@ -7,7 +7,7 @@
import React from 'react'; import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react'; import { FormattedMessage } from '@kbn/i18n/react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { FileBasedFieldVisConfig } from '../stats_table/types'; import { FileBasedFieldVisConfig } from '../stats_table/types';
export const FileBasedNumberContentPreview = ({ config }: { config: FileBasedFieldVisConfig }) => { export const FileBasedNumberContentPreview = ({ config }: { config: FileBasedFieldVisConfig }) => {
@ -23,28 +23,34 @@ export const FileBasedNumberContentPreview = ({ config }: { config: FileBasedFie
<EuiFlexGroup direction={'column'} gutterSize={'xs'}> <EuiFlexGroup direction={'column'} gutterSize={'xs'}>
<EuiFlexGroup gutterSize="xs"> <EuiFlexGroup gutterSize="xs">
<EuiFlexItem> <EuiFlexItem>
<b> <EuiText size={'xs'}>
<FormattedMessage id="xpack.dataVisualizer.fieldStats.minTitle" defaultMessage="min" /> <FormattedMessage id="xpack.dataVisualizer.fieldStats.minTitle" defaultMessage="min" />
</b> </EuiText>
</EuiFlexItem> </EuiFlexItem>
<EuiFlexItem> <EuiFlexItem>
<b> <EuiText size={'xs'}>
<FormattedMessage <FormattedMessage
id="xpack.dataVisualizer.fieldStats.medianTitle" id="xpack.dataVisualizer.fieldStats.medianTitle"
defaultMessage="median" defaultMessage="median"
/> />
</b> </EuiText>
</EuiFlexItem> </EuiFlexItem>
<EuiFlexItem> <EuiFlexItem>
<b> <EuiText size={'xs'}>
<FormattedMessage id="xpack.dataVisualizer.fieldStats.maxTitle" defaultMessage="max" /> <FormattedMessage id="xpack.dataVisualizer.fieldStats.maxTitle" defaultMessage="max" />
</b> </EuiText>
</EuiFlexItem> </EuiFlexItem>
</EuiFlexGroup> </EuiFlexGroup>
<EuiFlexGroup gutterSize="xs"> <EuiFlexGroup gutterSize="xs">
<EuiFlexItem>{stats.min}</EuiFlexItem> <EuiFlexItem>
<EuiFlexItem>{stats.median}</EuiFlexItem> <EuiText size={'xs'}>{stats.min}</EuiText>
<EuiFlexItem>{stats.max}</EuiFlexItem> </EuiFlexItem>
<EuiFlexItem>
<EuiText size={'xs'}>{stats.median}</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiText size={'xs'}>{stats.max}</EuiText>
</EuiFlexItem>
</EuiFlexGroup> </EuiFlexGroup>
</EuiFlexGroup> </EuiFlexGroup>
); );

View file

@ -2,6 +2,7 @@
exports[`FieldTypeIcon render component when type matches a field type 1`] = ` exports[`FieldTypeIcon render component when type matches a field type 1`] = `
<EuiToolTip <EuiToolTip
anchorClassName="dvFieldTypeIcon__anchor"
content="keyword type" content="keyword type"
delay="regular" delay="regular"
display="inlineBlock" display="inlineBlock"
@ -9,8 +10,7 @@ exports[`FieldTypeIcon render component when type matches a field type 1`] = `
> >
<FieldTypeIconContainer <FieldTypeIconContainer
ariaLabel="keyword type" ariaLabel="keyword type"
color="euiColorVis0" iconType="tokenKeyword"
iconType="tokenText"
needsAria={false} needsAria={false}
/> />
</EuiToolTip> </EuiToolTip>

View file

@ -0,0 +1,4 @@
.dvFieldTypeIcon__anchor {
display: flex;
align-items: center;
}

View file

@ -0,0 +1 @@
@import 'field_type_icon';

View file

@ -26,11 +26,10 @@ describe('FieldTypeIcon', () => {
const typeIconComponent = mount( const typeIconComponent = mount(
<FieldTypeIcon type={JOB_FIELD_TYPES.KEYWORD} tooltipEnabled={true} needsAria={false} /> <FieldTypeIcon type={JOB_FIELD_TYPES.KEYWORD} tooltipEnabled={true} needsAria={false} />
); );
const container = typeIconComponent.find({ 'data-test-subj': 'fieldTypeIcon' });
expect(typeIconComponent.find('EuiToolTip').children()).toHaveLength(1); expect(typeIconComponent.find('EuiToolTip').children()).toHaveLength(1);
container.simulate('mouseover'); typeIconComponent.simulate('mouseover');
// Run the timers so the EuiTooltip will be visible // Run the timers so the EuiTooltip will be visible
jest.runAllTimers(); jest.runAllTimers();
@ -38,7 +37,7 @@ describe('FieldTypeIcon', () => {
typeIconComponent.update(); typeIconComponent.update();
expect(typeIconComponent.find('EuiToolTip').children()).toHaveLength(2); expect(typeIconComponent.find('EuiToolTip').children()).toHaveLength(2);
container.simulate('mouseout'); typeIconComponent.simulate('mouseout');
// Run the timers so the EuiTooltip will be hidden again // Run the timers so the EuiTooltip will be hidden again
jest.runAllTimers(); jest.runAllTimers();

View file

@ -6,91 +6,62 @@
*/ */
import React, { FC } from 'react'; import React, { FC } from 'react';
import { EuiToken, EuiToolTip } from '@elastic/eui'; import { EuiToken, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { getJobTypeAriaLabel } from '../../util/field_types_utils'; import { getJobTypeAriaLabel } from '../../util/field_types_utils';
import { JOB_FIELD_TYPES } from '../../../../../common';
import type { JobFieldType } from '../../../../../common'; import type { JobFieldType } from '../../../../../common';
import './_index.scss';
interface FieldTypeIconProps { interface FieldTypeIconProps {
tooltipEnabled: boolean; tooltipEnabled: boolean;
type: JobFieldType; type: JobFieldType;
fieldName?: string;
needsAria: boolean; needsAria: boolean;
} }
interface FieldTypeIconContainerProps { interface FieldTypeIconContainerProps {
ariaLabel: string | null; ariaLabel: string | null;
iconType: string; iconType: string;
color: string; color?: string;
needsAria: boolean; needsAria: boolean;
[key: string]: any; [key: string]: any;
} }
// defaultIcon => a unknown datatype
const defaultIcon = { iconType: 'questionInCircle', color: 'gray' };
// Extended & modified version of src/plugins/kibana_react/public/field_icon/field_icon.tsx
export const typeToEuiIconMap: Record<string, { iconType: string; color?: string }> = {
boolean: { iconType: 'tokenBoolean' },
// icon for an index pattern mapping conflict in discover
conflict: { iconType: 'alert', color: 'euiColorVis9' },
date: { iconType: 'tokenDate' },
date_range: { iconType: 'tokenDate' },
geo_point: { iconType: 'tokenGeo' },
geo_shape: { iconType: 'tokenGeo' },
ip: { iconType: 'tokenIP' },
ip_range: { iconType: 'tokenIP' },
// is a plugin's data type https://www.elastic.co/guide/en/elasticsearch/plugins/current/mapper-murmur3-usage.html
murmur3: { iconType: 'tokenFile' },
number: { iconType: 'tokenNumber' },
number_range: { iconType: 'tokenNumber' },
histogram: { iconType: 'tokenHistogram' },
_source: { iconType: 'editorCodeBlock', color: 'gray' },
string: { iconType: 'tokenString' },
text: { iconType: 'tokenString' },
keyword: { iconType: 'tokenKeyword' },
nested: { iconType: 'tokenNested' },
};
export const FieldTypeIcon: FC<FieldTypeIconProps> = ({ export const FieldTypeIcon: FC<FieldTypeIconProps> = ({
tooltipEnabled = false, tooltipEnabled = false,
type, type,
fieldName,
needsAria = true, needsAria = true,
}) => { }) => {
const ariaLabel = getJobTypeAriaLabel(type); const ariaLabel = getJobTypeAriaLabel(type);
const token = typeToEuiIconMap[type] || defaultIcon;
let iconType = 'questionInCircle'; const containerProps = { ...token, ariaLabel, needsAria };
let color = 'euiColorVis6';
switch (type) {
// Set icon types and colors
case JOB_FIELD_TYPES.BOOLEAN:
iconType = 'tokenBoolean';
color = 'euiColorVis5';
break;
case JOB_FIELD_TYPES.DATE:
iconType = 'tokenDate';
color = 'euiColorVis7';
break;
case JOB_FIELD_TYPES.GEO_POINT:
case JOB_FIELD_TYPES.GEO_SHAPE:
iconType = 'tokenGeo';
color = 'euiColorVis8';
break;
case JOB_FIELD_TYPES.TEXT:
iconType = 'document';
color = 'euiColorVis9';
break;
case JOB_FIELD_TYPES.IP:
iconType = 'tokenIP';
color = 'euiColorVis3';
break;
case JOB_FIELD_TYPES.KEYWORD:
iconType = 'tokenText';
color = 'euiColorVis0';
break;
case JOB_FIELD_TYPES.NUMBER:
iconType = 'tokenNumber';
color = fieldName !== undefined ? 'euiColorVis1' : 'euiColorVis2';
break;
case JOB_FIELD_TYPES.HISTOGRAM:
iconType = 'tokenHistogram';
color = 'euiColorVis7';
case JOB_FIELD_TYPES.UNKNOWN:
// Use defaults
break;
}
const containerProps = {
ariaLabel,
iconType,
color,
needsAria,
};
if (tooltipEnabled === true) { if (tooltipEnabled === true) {
// wrap the inner component inside <span> because EuiToolTip doesn't seem
// to support having another component directly inside the tooltip anchor
// see https://github.com/elastic/eui/issues/839
return ( return (
<EuiToolTip <EuiToolTip
position="left" position="left"
@ -98,6 +69,7 @@ export const FieldTypeIcon: FC<FieldTypeIconProps> = ({
defaultMessage: '{type} type', defaultMessage: '{type} type',
values: { type }, values: { type },
})} })}
anchorClassName="dvFieldTypeIcon__anchor"
> >
<FieldTypeIconContainer {...containerProps} /> <FieldTypeIconContainer {...containerProps} />
</EuiToolTip> </EuiToolTip>
@ -122,12 +94,15 @@ const FieldTypeIconContainer: FC<FieldTypeIconContainerProps> = ({
if (needsAria && ariaLabel) { if (needsAria && ariaLabel) {
wrapperProps['aria-label'] = ariaLabel; wrapperProps['aria-label'] = ariaLabel;
} }
return ( return (
<span data-test-subj="fieldTypeIcon" {...rest}> <EuiToken
<span {...wrapperProps}> iconType={iconType}
<EuiToken iconType={iconType} shape="square" size="s" color={color} /> color={color}
</span> shape="square"
</span> size="s"
data-test-subj="fieldTypeIcon"
{...wrapperProps}
{...rest}
/>
); );
}; };

View file

@ -14,7 +14,7 @@ import type {
FileBasedUnknownFieldVisConfig, FileBasedUnknownFieldVisConfig,
} from '../stats_table/types/field_vis_config'; } from '../stats_table/types/field_vis_config';
import { FieldTypeIcon } from '../field_type_icon'; import { FieldTypeIcon } from '../field_type_icon';
import { JOB_FIELD_TYPES_OPTIONS } from '../../../../../common'; import { jobTypeLabels } from '../../util/field_types_utils';
interface Props { interface Props {
fields: Array<FileBasedFieldVisConfig | FileBasedUnknownFieldVisConfig>; fields: Array<FileBasedFieldVisConfig | FileBasedUnknownFieldVisConfig>;
@ -39,27 +39,18 @@ export const DataVisualizerFieldTypesFilter: FC<Props> = ({
const fieldTypesTracker = new Set(); const fieldTypesTracker = new Set();
const fieldTypes: Option[] = []; const fieldTypes: Option[] = [];
fields.forEach(({ type }) => { fields.forEach(({ type }) => {
if ( if (type !== undefined && !fieldTypesTracker.has(type) && jobTypeLabels[type] !== undefined) {
type !== undefined && const label = jobTypeLabels[type];
!fieldTypesTracker.has(type) &&
JOB_FIELD_TYPES_OPTIONS[type] !== undefined
) {
const item = JOB_FIELD_TYPES_OPTIONS[type];
fieldTypesTracker.add(type); fieldTypesTracker.add(type);
fieldTypes.push({ fieldTypes.push({
value: type, value: type,
name: ( name: (
<EuiFlexGroup> <EuiFlexGroup>
<EuiFlexItem grow={true}> {item.name}</EuiFlexItem> <EuiFlexItem grow={true}> {label}</EuiFlexItem>
{type && ( {type && (
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<FieldTypeIcon <FieldTypeIcon type={type} tooltipEnabled={false} needsAria={true} />
type={type}
fieldName={item.name}
tooltipEnabled={false}
needsAria={true}
/>
</EuiFlexItem> </EuiFlexItem>
)} )}
</EuiFlexGroup> </EuiFlexGroup>

View file

@ -25,7 +25,7 @@ interface Props {
export const getDefaultDataVisualizerListState = (): DataVisualizerTableState => ({ export const getDefaultDataVisualizerListState = (): DataVisualizerTableState => ({
pageIndex: 0, pageIndex: 0,
pageSize: 10, pageSize: 25,
sortField: 'fieldName', sortField: 'fieldName',
sortDirection: 'asc', sortDirection: 'asc',
visibleFieldTypes: [], visibleFieldTypes: [],

View file

@ -98,7 +98,7 @@ export const MultiSelectPicker: FC<{
); );
return ( return (
<EuiFilterGroup data-test-subj={dataTestSubj}> <EuiFilterGroup data-test-subj={dataTestSubj} style={{ marginLeft: 8 }}>
<EuiPopover <EuiPopover
ownFocus ownFocus
data-test-subj={`${dataTestSubj}-popover`} data-test-subj={`${dataTestSubj}-popover`}

View file

@ -6,19 +6,16 @@
*/ */
import React, { FC, Fragment } from 'react'; import React, { FC, Fragment } from 'react';
import { EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; import { EuiIcon, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react'; import { FormattedMessage } from '@kbn/i18n/react';
export const NotInDocsContent: FC = () => ( export const NotInDocsContent: FC = () => (
<Fragment> <Fragment>
<EuiSpacer size="xxl" />
<EuiText textAlign="center"> <EuiText textAlign="center">
<EuiIcon type="alert" /> <EuiIcon type="alert" />
</EuiText> </EuiText>
<EuiText textAlign="center" size={'xs'}>
<EuiSpacer size="s" />
<EuiText textAlign="center">
<FormattedMessage <FormattedMessage
id="xpack.dataVisualizer.dataGrid.field.fieldNotInDocsLabel" id="xpack.dataVisualizer.dataGrid.field.fieldNotInDocsLabel"
defaultMessage="This field does not appear in any documents for the selected time range" defaultMessage="This field does not appear in any documents for the selected time range"

View file

@ -1,86 +0,0 @@
.fieldDataCard {
height: 420px;
box-shadow: none;
border-color: $euiBorderColor;
// Note the names of these styles need to match the type of the field they are displaying.
.boolean {
color: $euiColorVis5;
border-color: $euiColorVis5;
}
.date {
color: $euiColorVis7;
border-color: $euiColorVis7;
}
.document_count {
color: $euiColorVis2;
border-color: $euiColorVis2;
}
.geo_point {
color: $euiColorVis8;
border-color: $euiColorVis8;
}
.ip {
color: $euiColorVis3;
border-color: $euiColorVis3;
}
.keyword {
color: $euiColorVis0;
border-color: $euiColorVis0;
}
.number {
color: $euiColorVis1;
border-color: $euiColorVis1;
}
.text {
color: $euiColorVis9;
border-color: $euiColorVis9;
}
.type-other,
.unknown {
color: $euiColorVis6;
border-color: $euiColorVis6;
}
.fieldDataCard__content {
@include euiFontSizeS;
height: 385px;
overflow: hidden;
}
.fieldDataCard__codeContent {
@include euiCodeFont;
}
.fieldDataCard__geoContent {
z-index: auto;
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
position: relative;
.embPanel__content {
display: flex;
flex: 1 1 100%;
z-index: 1;
min-height: 0; // Absolute must for Firefox to scroll contents
}
}
.fieldDataCard__stats {
padding: $euiSizeS $euiSizeS 0 $euiSizeS;
text-align: center;
}
.fieldDataCard__valuesTitle {
text-transform: uppercase;
}
}

View file

@ -2,55 +2,90 @@
@import 'components/field_count_stats/index'; @import 'components/field_count_stats/index';
@import 'components/field_data_row/index'; @import 'components/field_data_row/index';
.dataVisualizerFieldExpandedRow { $panelWidthS: #{'max(20%, 225px)'};
$panelWidthL: #{'max(40%, 450px)'};
.dvExpandedRow {
padding-left: $euiSize * 4; padding-left: $euiSize * 4;
width: 100%; width: 100%;
.fieldDataCard__valuesTitle { .dvExpandedRow__fieldHeader {
text-transform: uppercase; text-transform: uppercase;
text-align: left; text-align: left;
color: $euiColorDarkShade; color: $euiColorDarkShade;
font-weight: bold; font-weight: bold;
padding-bottom: $euiSizeS; padding-bottom: $euiSizeS;
} }
.fieldDataCard__codeContent {
@include euiCodeFont;
}
} }
.dataVisualizer { @include euiBreakpoint('m', 'l', 'xl') {
.euiTableRow > .euiTableRowCell { .dvTable {
border-bottom: 0; .columnHeader__title {
border-top: $euiBorderThin; display: flex;
align-items: center;
}
.euiTableRow-isExpandedRow {
.euiTableRowCell {
background-color: $euiColorEmptyShade !important;
border-top: 0;
border-bottom: $euiBorderThin;
&:hover {
background-color: $euiColorEmptyShade !important;
}
} }
}
.dataVisualizerSummaryTable { .columnHeader__icon {
max-width: 350px; padding-right: $euiSizeXS;
min-width: 250px; }
.euiTableRow > .euiTableRowCell { .euiTableRow > .euiTableRowCell {
border-bottom: 0; border-bottom: 0;
border-top: $euiBorderThin;
} }
.euiTableHeaderCell {
display: none; .euiTableCellContent {
padding: $euiSizeXS;
}
.euiTableRow-isExpandedRow {
.euiTableRowCell {
background-color: $euiColorEmptyShade !important;
border-top: 0;
border-bottom: $euiBorderThin;
&:hover {
background-color: $euiColorEmptyShade !important;
}
}
}
.dvSummaryTable {
.euiTableRow > .euiTableRowCell {
border-bottom: 0;
}
.euiTableHeaderCell {
display: none;
}
}
.dvSummaryTable__wrapper {
min-width: $panelWidthS;
max-width: $panelWidthS;
}
.dvTopValues__wrapper {
min-width: fit-content;
}
.dvPanel__wrapper {
margin: $euiSizeXS $euiSizeM $euiSizeM 0;
&.dvPanel--compressed {
width: $panelWidthS;
}
&.dvPanel--uniform {
min-width: $panelWidthS;
max-width: $panelWidthS;
}
}
.dvMap__wrapper {
height: $euiSize * 15; //240px
}
.dvText__wrapper {
min-width: $panelWidthS;
} }
}
.dataVisualizerSummaryTableWrapper {
max-width: 300px;
}
.dataVisualizerMapWrapper {
min-height: 300px;
min-width: 600px;
} }
} }

View file

@ -9,7 +9,12 @@ import { EuiText } from '@elastic/eui';
import React from 'react'; import React from 'react';
export const ExpandedRowFieldHeader = ({ children }: { children: React.ReactNode }) => ( export const ExpandedRowFieldHeader = ({ children }: { children: React.ReactNode }) => (
<EuiText size="xs" color={'subdued'} className={'fieldDataCard__valuesTitle'}> <EuiText
size="xs"
color={'subdued'}
className={'dvExpandedRow__fieldHeader'}
textAlign={'center'}
>
{children} {children}
</EuiText> </EuiText>
); );

View file

@ -1,3 +1,12 @@
.dataVisualizerFieldCountContainer { .dvFieldCount__panel {
max-width: 300px; margin-left: $euiSizeXS;
@include euiBreakpoint('xs', 's') {
flex-direction: column;
align-items: flex-start;
}
}
.dvFieldCount__item {
max-width: 300px;
min-width: 300px;
} }

View file

@ -30,8 +30,9 @@ export const MetricFieldsCount: FC<MetricFieldsCountProps> = ({ metricsStats })
<EuiFlexGroup <EuiFlexGroup
gutterSize="s" gutterSize="s"
alignItems="center" alignItems="center"
className="dataVisualizerFieldCountContainer" className="dvFieldCount__item"
data-test-subj="dataVisualizerMetricFieldsSummary" data-test-subj="dataVisualizerMetricFieldsSummary"
responsive={false}
> >
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<EuiText> <EuiText>

View file

@ -30,8 +30,9 @@ export const TotalFieldsCount: FC<TotalFieldsCountProps> = ({ fieldsCountStats }
<EuiFlexGroup <EuiFlexGroup
gutterSize="s" gutterSize="s"
alignItems="center" alignItems="center"
className="dataVisualizerFieldCountContainer" className="dvFieldCount__item"
data-test-subj="dataVisualizerFieldsSummary" data-test-subj="dataVisualizerFieldsSummary"
responsive={false}
> >
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<EuiText> <EuiText>

View file

@ -6,7 +6,7 @@
*/ */
import React, { FC, ReactNode, useMemo } from 'react'; import React, { FC, ReactNode, useMemo } from 'react';
import { EuiBasicTable, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { EuiBasicTable, EuiSpacer, RIGHT_ALIGNMENT, HorizontalAlignment } from '@elastic/eui';
import { Axis, BarSeries, Chart, Settings } from '@elastic/charts'; import { Axis, BarSeries, Chart, Settings } from '@elastic/charts';
import { FormattedMessage } from '@kbn/i18n/react'; import { FormattedMessage } from '@kbn/i18n/react';
@ -18,6 +18,7 @@ import { roundToDecimalPlace } from '../../../utils';
import { useDataVizChartTheme } from '../../hooks'; import { useDataVizChartTheme } from '../../hooks';
import { DocumentStatsTable } from './document_stats'; import { DocumentStatsTable } from './document_stats';
import { ExpandedRowContent } from './expanded_row_content'; import { ExpandedRowContent } from './expanded_row_content';
import { ExpandedRowPanel } from './expanded_row_panel';
function getPercentLabel(value: number): string { function getPercentLabel(value: number): string {
if (value === 0) { if (value === 0) {
@ -35,7 +36,7 @@ function getFormattedValue(value: number, totalCount: number): string {
return `${value} (${getPercentLabel(percentage)})`; return `${value} (${getPercentLabel(percentage)})`;
} }
const BOOLEAN_DISTRIBUTION_CHART_HEIGHT = 100; const BOOLEAN_DISTRIBUTION_CHART_HEIGHT = 70;
export const BooleanContent: FC<FieldDataRowProps> = ({ config }) => { export const BooleanContent: FC<FieldDataRowProps> = ({ config }) => {
const fieldFormat = 'fieldFormat' in config ? config.fieldFormat : undefined; const fieldFormat = 'fieldFormat' in config ? config.fieldFormat : undefined;
@ -68,9 +69,11 @@ export const BooleanContent: FC<FieldDataRowProps> = ({ config }) => {
]; ];
const summaryTableColumns = [ const summaryTableColumns = [
{ {
field: 'function',
name: '', name: '',
render: (summaryItem: { display: ReactNode }) => summaryItem.display, render: (_: string, summaryItem: { display: ReactNode }) => summaryItem.display,
width: '75px', width: '25px',
align: RIGHT_ALIGNMENT as HorizontalAlignment,
}, },
{ {
field: 'value', field: 'value',
@ -90,18 +93,18 @@ export const BooleanContent: FC<FieldDataRowProps> = ({ config }) => {
<ExpandedRowContent dataTestSubj={'dataVisualizerBooleanContent'}> <ExpandedRowContent dataTestSubj={'dataVisualizerBooleanContent'}>
<DocumentStatsTable config={config} /> <DocumentStatsTable config={config} />
<EuiFlexItem className={'dataVisualizerSummaryTableWrapper'}> <ExpandedRowPanel className={'dvSummaryTable__wrapper dvPanel__wrapper'}>
<ExpandedRowFieldHeader>{summaryTableTitle}</ExpandedRowFieldHeader> <ExpandedRowFieldHeader>{summaryTableTitle}</ExpandedRowFieldHeader>
<EuiBasicTable <EuiBasicTable
className={'dataVisualizerSummaryTable'} className={'dvSummaryTable'}
compressed compressed
items={summaryTableItems} items={summaryTableItems}
columns={summaryTableColumns} columns={summaryTableColumns}
tableCaption={summaryTableTitle} tableCaption={summaryTableTitle}
/> />
</EuiFlexItem> </ExpandedRowPanel>
<EuiFlexItem> <ExpandedRowPanel className={'dvPanel__wrapper dvPanel--uniform'}>
<ExpandedRowFieldHeader> <ExpandedRowFieldHeader>
<FormattedMessage <FormattedMessage
id="xpack.dataVisualizer.dataGrid.field.cardBoolean.valuesLabel" id="xpack.dataVisualizer.dataGrid.field.cardBoolean.valuesLabel"
@ -139,7 +142,7 @@ export const BooleanContent: FC<FieldDataRowProps> = ({ config }) => {
yScaleType="linear" yScaleType="linear"
/> />
</Chart> </Chart>
</EuiFlexItem> </ExpandedRowPanel>
</ExpandedRowContent> </ExpandedRowContent>
); );
}; };

View file

@ -6,7 +6,7 @@
*/ */
import React, { FC, useMemo } from 'react'; import React, { FC, useMemo } from 'react';
import { EuiFlexItem, EuiSpacer, EuiText, htmlIdGenerator } from '@elastic/eui'; import { EuiSpacer, EuiText, htmlIdGenerator } from '@elastic/eui';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react'; import { FormattedMessage } from '@kbn/i18n/react';
import { import {
@ -20,6 +20,7 @@ import {
import { EMSTermJoinConfig } from '../../../../../../../../maps/public'; import { EMSTermJoinConfig } from '../../../../../../../../maps/public';
import { EmbeddedMapComponent } from '../../../embedded_map'; import { EmbeddedMapComponent } from '../../../embedded_map';
import { FieldVisStats } from '../../../../../../../common/types'; import { FieldVisStats } from '../../../../../../../common/types';
import { ExpandedRowPanel } from './expanded_row_panel';
export const getChoroplethTopValuesLayer = ( export const getChoroplethTopValuesLayer = (
fieldName: string, fieldName: string,
@ -104,14 +105,19 @@ export const ChoroplethMap: FC<Props> = ({ stats, suggestion }) => {
); );
return ( return (
<EuiFlexItem data-test-subj={'fileDataVisualizerChoroplethMapTopValues'}> <ExpandedRowPanel
<div style={{ width: '100%', minHeight: 300 }}> dataTestSubj={'fileDataVisualizerChoroplethMapTopValues'}
className={'dvPanel__wrapper'}
grow={true}
>
<div className={'dvMap__wrapper'}>
<EmbeddedMapComponent layerList={layerList} /> <EmbeddedMapComponent layerList={layerList} />
</div> </div>
{isTopValuesSampled === true && ( {isTopValuesSampled === true && (
<> <div>
<EuiSpacer size="xs" /> <EuiSpacer size={'s'} />
<EuiText size="xs" textAlign={'left'}> <EuiText size="xs" textAlign={'center'}>
<FormattedMessage <FormattedMessage
id="xpack.dataVisualizer.dataGrid.fieldExpandedRow.choroplethMapTopValues.calculatedFromSampleDescription" id="xpack.dataVisualizer.dataGrid.fieldExpandedRow.choroplethMapTopValues.calculatedFromSampleDescription"
defaultMessage="Calculated from sample of {topValuesSamplerShardSize} documents per shard" defaultMessage="Calculated from sample of {topValuesSamplerShardSize} documents per shard"
@ -120,8 +126,8 @@ export const ChoroplethMap: FC<Props> = ({ stats, suggestion }) => {
}} }}
/> />
</EuiText> </EuiText>
</> </div>
)} )}
</EuiFlexItem> </ExpandedRowPanel>
); );
}; };

View file

@ -6,16 +6,18 @@
*/ */
import React, { FC, ReactNode } from 'react'; import React, { FC, ReactNode } from 'react';
import { EuiBasicTable, EuiFlexItem } from '@elastic/eui'; import { EuiBasicTable, HorizontalAlignment } from '@elastic/eui';
// @ts-ignore // @ts-ignore
import { formatDate } from '@elastic/eui/lib/services/format'; import { formatDate } from '@elastic/eui/lib/services/format';
import { FormattedMessage } from '@kbn/i18n/react'; import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { RIGHT_ALIGNMENT } from '@elastic/eui';
import type { FieldDataRowProps } from '../../types/field_data_row'; import type { FieldDataRowProps } from '../../types/field_data_row';
import { ExpandedRowFieldHeader } from '../expanded_row_field_header'; import { ExpandedRowFieldHeader } from '../expanded_row_field_header';
import { DocumentStatsTable } from './document_stats'; import { DocumentStatsTable } from './document_stats';
import { ExpandedRowContent } from './expanded_row_content'; import { ExpandedRowContent } from './expanded_row_content';
import { ExpandedRowPanel } from './expanded_row_panel';
const TIME_FORMAT = 'MMM D YYYY, HH:mm:ss.SSS'; const TIME_FORMAT = 'MMM D YYYY, HH:mm:ss.SSS';
interface SummaryTableItem { interface SummaryTableItem {
function: string; function: string;
@ -60,8 +62,10 @@ export const DateContent: FC<FieldDataRowProps> = ({ config }) => {
const summaryTableColumns = [ const summaryTableColumns = [
{ {
name: '', name: '',
render: (summaryItem: { display: ReactNode }) => summaryItem.display, field: 'function',
width: '75px', render: (func: string, summaryItem: { display: ReactNode }) => summaryItem.display,
width: '70px',
align: RIGHT_ALIGNMENT as HorizontalAlignment,
}, },
{ {
field: 'value', field: 'value',
@ -73,10 +77,10 @@ export const DateContent: FC<FieldDataRowProps> = ({ config }) => {
return ( return (
<ExpandedRowContent dataTestSubj={'dataVisualizerDateContent'}> <ExpandedRowContent dataTestSubj={'dataVisualizerDateContent'}>
<DocumentStatsTable config={config} /> <DocumentStatsTable config={config} />
<EuiFlexItem className={'dataVisualizerSummaryTableWrapper'}> <ExpandedRowPanel className={'dvSummaryTable__wrapper dvPanel__wrapper'}>
<ExpandedRowFieldHeader>{summaryTableTitle}</ExpandedRowFieldHeader> <ExpandedRowFieldHeader>{summaryTableTitle}</ExpandedRowFieldHeader>
<EuiBasicTable<SummaryTableItem> <EuiBasicTable<SummaryTableItem>
className={'dataVisualizerSummaryTable'} className={'dvSummaryTable'}
data-test-subj={'dataVisualizerDateSummaryTable'} data-test-subj={'dataVisualizerDateSummaryTable'}
compressed compressed
items={summaryTableItems} items={summaryTableItems}
@ -84,7 +88,7 @@ export const DateContent: FC<FieldDataRowProps> = ({ config }) => {
tableCaption={summaryTableTitle} tableCaption={summaryTableTitle}
tableLayout="auto" tableLayout="auto"
/> />
</EuiFlexItem> </ExpandedRowPanel>
</ExpandedRowContent> </ExpandedRowContent>
); );
}; };

View file

@ -8,16 +8,19 @@
import { FormattedMessage } from '@kbn/i18n/react'; import { FormattedMessage } from '@kbn/i18n/react';
import React, { FC, ReactNode } from 'react'; import React, { FC, ReactNode } from 'react';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { EuiBasicTable, EuiFlexItem } from '@elastic/eui'; import { EuiBasicTable, HorizontalAlignment, RIGHT_ALIGNMENT } from '@elastic/eui';
import { ExpandedRowFieldHeader } from '../expanded_row_field_header'; import { ExpandedRowFieldHeader } from '../expanded_row_field_header';
import { FieldDataRowProps } from '../../types'; import { FieldDataRowProps } from '../../types';
import { roundToDecimalPlace } from '../../../utils'; import { roundToDecimalPlace } from '../../../utils';
import { ExpandedRowPanel } from './expanded_row_panel';
const metaTableColumns = [ const metaTableColumns = [
{ {
field: 'function',
name: '', name: '',
render: (metaItem: { display: ReactNode }) => metaItem.display, render: (_: string, metaItem: { display: ReactNode }) => metaItem.display,
width: '75px', width: '25px',
align: RIGHT_ALIGNMENT as HorizontalAlignment,
}, },
{ {
field: 'value', field: 'value',
@ -76,18 +79,18 @@ export const DocumentStatsTable: FC<FieldDataRowProps> = ({ config }) => {
]; ];
return ( return (
<EuiFlexItem <ExpandedRowPanel
data-test-subj={'dataVisualizerDocumentStatsContent'} dataTestSubj={'dataVisualizerDocumentStatsContent'}
className={'dataVisualizerSummaryTableWrapper'} className={'dvSummaryTable__wrapper dvPanel__wrapper'}
> >
<ExpandedRowFieldHeader>{metaTableTitle}</ExpandedRowFieldHeader> <ExpandedRowFieldHeader>{metaTableTitle}</ExpandedRowFieldHeader>
<EuiBasicTable <EuiBasicTable
className={'dataVisualizerSummaryTable'} className={'dvSummaryTable'}
compressed compressed
items={metaTableItems} items={metaTableItems}
columns={metaTableColumns} columns={metaTableColumns}
tableCaption={metaTableTitle} tableCaption={metaTableTitle}
/> />
</EuiFlexItem> </ExpandedRowPanel>
); );
}; };

View file

@ -6,7 +6,7 @@
*/ */
import React, { FC, ReactNode } from 'react'; import React, { FC, ReactNode } from 'react';
import { EuiFlexGroup } from '@elastic/eui'; import { EuiFlexGrid } from '@elastic/eui';
interface Props { interface Props {
children: ReactNode; children: ReactNode;
@ -14,12 +14,8 @@ interface Props {
} }
export const ExpandedRowContent: FC<Props> = ({ children, dataTestSubj }) => { export const ExpandedRowContent: FC<Props> = ({ children, dataTestSubj }) => {
return ( return (
<EuiFlexGroup <EuiFlexGrid data-test-subj={dataTestSubj} gutterSize={'s'}>
data-test-subj={dataTestSubj}
gutterSize={'xl'}
className={'dataVisualizerExpandedRow'}
>
{children} {children}
</EuiFlexGroup> </EuiFlexGrid>
); );
}; };

View file

@ -0,0 +1,31 @@
/*
* 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, { FC, ReactNode } from 'react';
import { EuiPanel } from '@elastic/eui';
import { EuiFlexItemProps } from '@elastic/eui/src/components/flex/flex_item';
interface Props {
children: ReactNode;
dataTestSubj?: string;
grow?: EuiFlexItemProps['grow'];
className?: string;
}
export const ExpandedRowPanel: FC<Props> = ({ children, dataTestSubj, grow, className }) => {
return (
<EuiPanel
data-test-subj={dataTestSubj}
hasShadow={false}
hasBorder={true}
grow={!!grow}
className={className ?? ''}
paddingSize={'s'}
>
{children}
</EuiPanel>
);
};

View file

@ -11,7 +11,7 @@ import { TopValues } from '../../../top_values';
import { DocumentStatsTable } from './document_stats'; import { DocumentStatsTable } from './document_stats';
import { ExpandedRowContent } from './expanded_row_content'; import { ExpandedRowContent } from './expanded_row_content';
export const IpContent: FC<FieldDataRowProps> = ({ config }) => { export const IpContent: FC<FieldDataRowProps> = ({ config, onAddFilter }) => {
const { stats } = config; const { stats } = config;
if (stats === undefined) return null; if (stats === undefined) return null;
const { count, sampleCount, cardinality } = stats; const { count, sampleCount, cardinality } = stats;
@ -21,7 +21,12 @@ export const IpContent: FC<FieldDataRowProps> = ({ config }) => {
return ( return (
<ExpandedRowContent dataTestSubj={'dataVisualizerIPContent'}> <ExpandedRowContent dataTestSubj={'dataVisualizerIPContent'}>
<DocumentStatsTable config={config} /> <DocumentStatsTable config={config} />
<TopValues stats={stats} fieldFormat={fieldFormat} barColor="secondary" /> <TopValues
stats={stats}
fieldFormat={fieldFormat}
barColor="secondary"
onAddFilter={onAddFilter}
/>
</ExpandedRowContent> </ExpandedRowContent>
); );
}; };

View file

@ -14,7 +14,7 @@ import { DocumentStatsTable } from './document_stats';
import { ExpandedRowContent } from './expanded_row_content'; import { ExpandedRowContent } from './expanded_row_content';
import { ChoroplethMap } from './choropleth_map'; import { ChoroplethMap } from './choropleth_map';
export const KeywordContent: FC<FieldDataRowProps> = ({ config }) => { export const KeywordContent: FC<FieldDataRowProps> = ({ config, onAddFilter }) => {
const [EMSSuggestion, setEMSSuggestion] = useState<EMSTermJoinConfig | null | undefined>(); const [EMSSuggestion, setEMSSuggestion] = useState<EMSTermJoinConfig | null | undefined>();
const { stats, fieldName } = config; const { stats, fieldName } = config;
const fieldFormat = 'fieldFormat' in config ? config.fieldFormat : undefined; const fieldFormat = 'fieldFormat' in config ? config.fieldFormat : undefined;
@ -44,7 +44,12 @@ export const KeywordContent: FC<FieldDataRowProps> = ({ config }) => {
return ( return (
<ExpandedRowContent dataTestSubj={'dataVisualizerKeywordContent'}> <ExpandedRowContent dataTestSubj={'dataVisualizerKeywordContent'}>
<DocumentStatsTable config={config} /> <DocumentStatsTable config={config} />
<TopValues stats={stats} fieldFormat={fieldFormat} barColor="secondary" /> <TopValues
stats={stats}
fieldFormat={fieldFormat}
barColor="secondary"
onAddFilter={onAddFilter}
/>
{EMSSuggestion && stats && <ChoroplethMap stats={stats} suggestion={EMSSuggestion} />} {EMSSuggestion && stats && <ChoroplethMap stats={stats} suggestion={EMSSuggestion} />}
</ExpandedRowContent> </ExpandedRowContent>
); );

View file

@ -6,7 +6,13 @@
*/ */
import React, { FC, ReactNode, useEffect, useState } from 'react'; import React, { FC, ReactNode, useEffect, useState } from 'react';
import { EuiBasicTable, EuiFlexItem, EuiText } from '@elastic/eui'; import {
EuiBasicTable,
EuiFlexItem,
EuiText,
HorizontalAlignment,
RIGHT_ALIGNMENT,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react'; import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
@ -21,8 +27,9 @@ import { TopValues } from '../../../top_values';
import { ExpandedRowFieldHeader } from '../expanded_row_field_header'; import { ExpandedRowFieldHeader } from '../expanded_row_field_header';
import { DocumentStatsTable } from './document_stats'; import { DocumentStatsTable } from './document_stats';
import { ExpandedRowContent } from './expanded_row_content'; import { ExpandedRowContent } from './expanded_row_content';
import { ExpandedRowPanel } from './expanded_row_panel';
const METRIC_DISTRIBUTION_CHART_WIDTH = 325; const METRIC_DISTRIBUTION_CHART_WIDTH = 260;
const METRIC_DISTRIBUTION_CHART_HEIGHT = 200; const METRIC_DISTRIBUTION_CHART_HEIGHT = 200;
interface SummaryTableItem { interface SummaryTableItem {
@ -31,7 +38,7 @@ interface SummaryTableItem {
value: number | string | undefined | null; value: number | string | undefined | null;
} }
export const NumberContent: FC<FieldDataRowProps> = ({ config }) => { export const NumberContent: FC<FieldDataRowProps> = ({ config, onAddFilter }) => {
const { stats } = config; const { stats } = config;
useEffect(() => { useEffect(() => {
@ -83,7 +90,8 @@ export const NumberContent: FC<FieldDataRowProps> = ({ config }) => {
{ {
name: '', name: '',
render: (summaryItem: { display: ReactNode }) => summaryItem.display, render: (summaryItem: { display: ReactNode }) => summaryItem.display,
width: '75px', width: '25px',
align: RIGHT_ALIGNMENT as HorizontalAlignment,
}, },
{ {
field: 'value', field: 'value',
@ -101,23 +109,33 @@ export const NumberContent: FC<FieldDataRowProps> = ({ config }) => {
return ( return (
<ExpandedRowContent dataTestSubj={'dataVisualizerNumberContent'}> <ExpandedRowContent dataTestSubj={'dataVisualizerNumberContent'}>
<DocumentStatsTable config={config} /> <DocumentStatsTable config={config} />
<EuiFlexItem className={'dataVisualizerSummaryTableWrapper'}> <ExpandedRowPanel className={'dvSummaryTable__wrapper dvPanel__wrapper'} grow={1}>
<ExpandedRowFieldHeader>{summaryTableTitle}</ExpandedRowFieldHeader> <ExpandedRowFieldHeader>{summaryTableTitle}</ExpandedRowFieldHeader>
<EuiBasicTable<SummaryTableItem> <EuiBasicTable<SummaryTableItem>
className={'dataVisualizerSummaryTable'} className={'dvSummaryTable'}
compressed compressed
items={summaryTableItems} items={summaryTableItems}
columns={summaryTableColumns} columns={summaryTableColumns}
tableCaption={summaryTableTitle} tableCaption={summaryTableTitle}
data-test-subj={'dataVisualizerNumberSummaryTable'} data-test-subj={'dataVisualizerNumberSummaryTable'}
/> />
</EuiFlexItem> </ExpandedRowPanel>
{stats && ( {stats && (
<TopValues stats={stats} fieldFormat={fieldFormat} barColor="secondary" compressed={true} /> <TopValues
stats={stats}
fieldFormat={fieldFormat}
barColor="secondary"
compressed={true}
onAddFilter={onAddFilter}
/>
)} )}
{distribution && ( {distribution && (
<EuiFlexItem data-test-subj={'dataVisualizerFieldDataMetricDistribution'}> <ExpandedRowPanel
dataTestSubj={'dataVisualizerFieldDataMetricDistribution'}
className="dvPanel__wrapper"
grow={false}
>
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<ExpandedRowFieldHeader> <ExpandedRowFieldHeader>
<FormattedMessage <FormattedMessage
@ -136,7 +154,7 @@ export const NumberContent: FC<FieldDataRowProps> = ({ config }) => {
/> />
</EuiFlexItem> </EuiFlexItem>
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<EuiText size="xs"> <EuiText size="xs" textAlign={'center'}>
<FormattedMessage <FormattedMessage
id="xpack.dataVisualizer.dataGrid.fieldExpandedRow.numberContent.displayingPercentilesLabel" id="xpack.dataVisualizer.dataGrid.fieldExpandedRow.numberContent.displayingPercentilesLabel"
defaultMessage="Displaying {minPercent} - {maxPercent} percentiles" defaultMessage="Displaying {minPercent} - {maxPercent} percentiles"
@ -147,7 +165,7 @@ export const NumberContent: FC<FieldDataRowProps> = ({ config }) => {
/> />
</EuiText> </EuiText>
</EuiFlexItem> </EuiFlexItem>
</EuiFlexItem> </ExpandedRowPanel>
)} )}
</ExpandedRowContent> </ExpandedRowContent>
); );

View file

@ -6,7 +6,6 @@
*/ */
import React, { FC } from 'react'; import React, { FC } from 'react';
import { EuiFlexItem } from '@elastic/eui';
import type { FieldDataRowProps } from '../../types/field_data_row'; import type { FieldDataRowProps } from '../../types/field_data_row';
import { ExamplesList } from '../../../examples_list'; import { ExamplesList } from '../../../examples_list';
import { DocumentStatsTable } from './document_stats'; import { DocumentStatsTable } from './document_stats';
@ -15,14 +14,12 @@ import { ExpandedRowContent } from './expanded_row_content';
export const OtherContent: FC<FieldDataRowProps> = ({ config }) => { export const OtherContent: FC<FieldDataRowProps> = ({ config }) => {
const { stats } = config; const { stats } = config;
if (stats === undefined) return null; if (stats === undefined) return null;
return ( return stats.count === undefined ? (
<>{Array.isArray(stats.examples) && <ExamplesList examples={stats.examples} />}</>
) : (
<ExpandedRowContent dataTestSubj={'dataVisualizerOtherContent'}> <ExpandedRowContent dataTestSubj={'dataVisualizerOtherContent'}>
<DocumentStatsTable config={config} /> <DocumentStatsTable config={config} />
{Array.isArray(stats.examples) && ( {Array.isArray(stats.examples) && <ExamplesList examples={stats.examples} />}
<EuiFlexItem>
<ExamplesList examples={stats.examples} />
</EuiFlexItem>
)}
</ExpandedRowContent> </ExpandedRowContent>
); );
}; };

View file

@ -6,7 +6,7 @@
*/ */
import React, { FC, Fragment } from 'react'; import React, { FC, Fragment } from 'react';
import { EuiCallOut, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { EuiCallOut, EuiSpacer, EuiFlexItem } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react'; import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
@ -26,7 +26,7 @@ export const TextContent: FC<FieldDataRowProps> = ({ config }) => {
return ( return (
<ExpandedRowContent dataTestSubj={'dataVisualizerTextContent'}> <ExpandedRowContent dataTestSubj={'dataVisualizerTextContent'}>
<EuiFlexItem> <EuiFlexItem grow={false} className="dvText__wrapper">
{numExamples > 0 && <ExamplesList examples={examples} />} {numExamples > 0 && <ExamplesList examples={examples} />}
{numExamples === 0 && ( {numExamples === 0 && (
<Fragment> <Fragment>
@ -44,7 +44,7 @@ export const TextContent: FC<FieldDataRowProps> = ({ config }) => {
id="xpack.dataVisualizer.dataGrid.fieldText.fieldNotPresentDescription" id="xpack.dataVisualizer.dataGrid.fieldText.fieldNotPresentDescription"
defaultMessage="This field was not present in the {sourceParam} field of documents queried." defaultMessage="This field was not present in the {sourceParam} field of documents queried."
values={{ values={{
sourceParam: <span className="fieldDataCard__codeContent">_source</span>, sourceParam: <span className="dvExpandedRow__codeContent">_source</span>,
}} }}
/> />
@ -54,10 +54,10 @@ export const TextContent: FC<FieldDataRowProps> = ({ config }) => {
id="xpack.dataVisualizer.dataGrid.fieldText.fieldMayBePopulatedDescription" id="xpack.dataVisualizer.dataGrid.fieldText.fieldMayBePopulatedDescription"
defaultMessage="It may be populated, for example, using a {copyToParam} parameter in the document mapping, or be pruned from the {sourceParam} field after indexing through the use of {includesParam} and {excludesParam} parameters." defaultMessage="It may be populated, for example, using a {copyToParam} parameter in the document mapping, or be pruned from the {sourceParam} field after indexing through the use of {includesParam} and {excludesParam} parameters."
values={{ values={{
copyToParam: <span className="fieldDataCard__codeContent">copy_to</span>, copyToParam: <span className="dvExpandedRow__codeContent">copy_to</span>,
sourceParam: <span className="fieldDataCard__codeContent">_source</span>, sourceParam: <span className="dvExpandedRow__codeContent">_source</span>,
includesParam: <span className="fieldDataCard__codeContent">includes</span>, includesParam: <span className="dvExpandedRow__codeContent">includes</span>,
excludesParam: <span className="fieldDataCard__codeContent">excludes</span>, excludesParam: <span className="dvExpandedRow__codeContent">excludes</span>,
}} }}
/> />
</EuiCallOut> </EuiCallOut>

View file

@ -1,19 +1,22 @@
.dataGridChart__histogram { .dataGridChart__histogram {
width: 100%; width: 100%;
height: $euiSizeXL + $euiSizeXXL; }
.dataGridChart__column-chart {
width: 100%;
} }
.dataGridChart__legend { .dataGridChart__legend {
@include euiTextTruncate; @include euiTextTruncate;
@include euiFontSizeXS;
color: $euiColorMediumShade; color: $euiColorMediumShade;
display: block; display: block;
overflow-x: hidden; overflow-x: hidden;
margin: $euiSizeXS 0 0 0;
font-style: italic; font-style: italic;
font-weight: normal; font-weight: normal;
text-align: left; text-align: left;
line-height: 1.1;
font-size: #{$euiFontSizeL / 2}; // 10px
} }
.dataGridChart__legend--numeric { .dataGridChart__legend--numeric {
@ -21,9 +24,7 @@
} }
.dataGridChart__legendBoolean { .dataGridChart__legendBoolean {
width: 100%; width: #{$euiSizeXS * 2.5} // 10px
min-width: $euiButtonMinWidth;
td { text-align: center }
} }
/* Override to align column header to bottom of cell when no chart is available */ /* Override to align column header to bottom of cell when no chart is available */

View file

@ -8,7 +8,7 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { BarSeries, Chart, Settings } from '@elastic/charts'; import { Axis, BarSeries, Chart, Position, ScaleType, Settings } from '@elastic/charts';
import { EuiDataGridColumn } from '@elastic/eui'; import { EuiDataGridColumn } from '@elastic/eui';
import './column_chart.scss'; import './column_chart.scss';
@ -25,22 +25,9 @@ interface Props {
maxChartColumns?: number; maxChartColumns?: number;
} }
const columnChartTheme = { const zeroSize = { bottom: 0, left: 0, right: 0, top: 0 };
background: { color: 'transparent' }, const size = { width: 100, height: 10 };
chartMargins: {
left: 0,
right: 0,
top: 0,
bottom: 1,
},
chartPaddings: {
left: 0,
right: 0,
top: 0,
bottom: 0,
},
scales: { barsPadding: 0.1 },
};
export const ColumnChart: FC<Props> = ({ export const ColumnChart: FC<Props> = ({
chartData, chartData,
columnType, columnType,
@ -48,26 +35,34 @@ export const ColumnChart: FC<Props> = ({
hideLabel, hideLabel,
maxChartColumns, maxChartColumns,
}) => { }) => {
const { data, legendText, xScaleType } = useColumnChart(chartData, columnType, maxChartColumns); const { data, legendText } = useColumnChart(chartData, columnType, maxChartColumns);
return ( return (
<div data-test-subj={dataTestSubj}> <div data-test-subj={dataTestSubj}>
{!isUnsupportedChartData(chartData) && data.length > 0 && ( {!isUnsupportedChartData(chartData) && data.length > 0 && (
<div className="dataGridChart__histogram" data-test-subj={`${dataTestSubj}-histogram`}> <Chart size={size}>
<Chart> <Settings
<Settings theme={columnChartTheme} /> xDomain={{ min: 0, max: 9 }}
<BarSeries theme={{ chartMargins: zeroSize, chartPaddings: zeroSize }}
id="histogram" />
name="count" <Axis
xScaleType={xScaleType} id="bottom"
yScaleType="linear" position={Position.Bottom}
xAccessor={'key_as_string'} tickFormat={(idx) => {
yAccessors={['doc_count']} return `${data[idx]?.key_as_string ?? ''}`;
styleAccessor={(d) => d.datum.color} }}
data={data} hide
/> />
</Chart> <BarSeries
</div> id={'count'}
xScaleType={ScaleType.Linear}
yScaleType={ScaleType.Linear}
xAccessor="x"
yAccessors={['doc_count']}
data={data}
styleAccessor={(d) => d.datum.color}
/>
</Chart>
)} )}
<div <div
className={classNames('dataGridChart__legend', { className={classNames('dataGridChart__legend', {

View file

@ -5,20 +5,21 @@
* 2.0. * 2.0.
*/ */
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui'; import { EuiIcon, EuiText } from '@elastic/eui';
import React from 'react'; import React from 'react';
export const DistinctValues = ({ cardinality }: { cardinality?: number }) => { interface Props {
cardinality?: number;
showIcon?: boolean;
}
export const DistinctValues = ({ cardinality, showIcon }: Props) => {
if (cardinality === undefined) return null; if (cardinality === undefined) return null;
return ( return (
<EuiFlexGroup alignItems={'center'}> <>
<EuiFlexItem className={'dataVisualizerColumnHeaderIcon'}> {showIcon ? <EuiIcon type="database" size={'m'} className={'columnHeader__icon'} /> : null}
<EuiIcon type="database" size={'s'} /> <EuiText size={'xs'}>{cardinality}</EuiText>
</EuiFlexItem> </>
<EuiText size={'s'}>
<b>{cardinality}</b>
</EuiText>
</EuiFlexGroup>
); );
}; };

View file

@ -5,29 +5,36 @@
* 2.0. * 2.0.
*/ */
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui'; import { EuiIcon, EuiText } from '@elastic/eui';
import React from 'react'; import React from 'react';
import type { FieldDataRowProps } from '../../types/field_data_row'; import type { FieldDataRowProps } from '../../types/field_data_row';
import { roundToDecimalPlace } from '../../../utils'; import { roundToDecimalPlace } from '../../../utils';
import { isIndexBasedFieldVisConfig } from '../../types';
export const DocumentStat = ({ config }: FieldDataRowProps) => { interface Props extends FieldDataRowProps {
showIcon?: boolean;
}
export const DocumentStat = ({ config, showIcon }: Props) => {
const { stats } = config; const { stats } = config;
if (stats === undefined) return null; if (stats === undefined) return null;
const { count, sampleCount } = stats; const { count, sampleCount } = stats;
if (count === undefined || sampleCount === undefined) return null;
const docsPercent = roundToDecimalPlace((count / sampleCount) * 100); // If field exists is docs but we don't have count stats then don't show
// Otherwise if field doesn't appear in docs at all, show 0%
const docsCount =
count ?? (isIndexBasedFieldVisConfig(config) && config.existsInDocs === true ? undefined : 0);
const docsPercent =
docsCount !== undefined && sampleCount !== undefined
? roundToDecimalPlace((docsCount / sampleCount) * 100)
: 0;
return ( return docsCount !== undefined ? (
<EuiFlexGroup alignItems={'center'}> <>
<EuiFlexItem className={'dataVisualizerColumnHeaderIcon'}> {showIcon ? <EuiIcon type="document" size={'m'} className={'columnHeader__icon'} /> : null}
<EuiIcon type="document" size={'s'} /> <EuiText size={'xs'}>
</EuiFlexItem> {docsCount} ({docsPercent}%)
<EuiText size={'s'}>
<b>{count}</b> ({docsPercent}%)
</EuiText> </EuiText>
</EuiFlexGroup> </>
); ) : null;
}; };

View file

@ -6,7 +6,7 @@
*/ */
import React, { FC, useEffect, useState } from 'react'; import React, { FC, useEffect, useState } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import classNames from 'classnames'; import classNames from 'classnames';
import { import {
MetricDistributionChart, MetricDistributionChart,
@ -16,8 +16,8 @@ import {
import { FieldVisConfig } from '../../types'; import { FieldVisConfig } from '../../types';
import { kibanaFieldFormat, formatSingleValue } from '../../../utils'; import { kibanaFieldFormat, formatSingleValue } from '../../../utils';
const METRIC_DISTRIBUTION_CHART_WIDTH = 150; const METRIC_DISTRIBUTION_CHART_WIDTH = 100;
const METRIC_DISTRIBUTION_CHART_HEIGHT = 80; const METRIC_DISTRIBUTION_CHART_HEIGHT = 10;
export interface NumberContentPreviewProps { export interface NumberContentPreviewProps {
config: FieldVisConfig; config: FieldVisConfig;
@ -59,8 +59,11 @@ export const IndexBasedNumberContentPreview: FC<NumberContentPreviewProps> = ({
<div className={'dataGridChart__legend'} data-test-subj={`${dataTestSubj}-legend`}> <div className={'dataGridChart__legend'} data-test-subj={`${dataTestSubj}-legend`}>
{legendText && ( {legendText && (
<> <>
<EuiSpacer size="s" /> <EuiFlexGroup
<EuiFlexGroup direction={'row'} data-test-subj={`${dataTestSubj}-legend`}> direction={'row'}
data-test-subj={`${dataTestSubj}-legend`}
responsive={false}
>
<EuiFlexItem className={'dataGridChart__legend'}> <EuiFlexItem className={'dataGridChart__legend'}>
{kibanaFieldFormat(legendText.min, fieldFormat)} {kibanaFieldFormat(legendText.min, fieldFormat)}
</EuiFlexItem> </EuiFlexItem>

View file

@ -122,8 +122,8 @@ describe('getLegendText()', () => {
})} })}
</> </>
); );
expect(getByText('true')).toBeInTheDocument(); expect(getByText('t')).toBeInTheDocument();
expect(getByText('false')).toBeInTheDocument(); expect(getByText('f')).toBeInTheDocument();
}); });
it('should return the chart legend text for ordinal chart data with less than max categories', () => { it('should return the chart legend text for ordinal chart data with less than max categories', () => {
expect(getLegendText({ ...validOrdinalChartData, data: [{ key: 'cat', doc_count: 10 }] })).toBe( expect(getLegendText({ ...validOrdinalChartData, data: [{ key: 'cat', doc_count: 10 }] })).toBe(

View file

@ -94,11 +94,19 @@ export const getLegendText = (
if (chartData.type === 'boolean') { if (chartData.type === 'boolean') {
return ( return (
<table className="dataGridChart__legendBoolean"> <table>
<tbody> <tbody>
<tr> <tr>
{chartData.data[0] !== undefined && <td>{chartData.data[0].key_as_string}</td>} {chartData.data[0] !== undefined && (
{chartData.data[1] !== undefined && <td>{chartData.data[1].key_as_string}</td>} <td className="dataGridChart__legendBoolean">
{chartData.data[0].key_as_string?.slice(0, 1) ?? ''}
</td>
)}
{chartData.data[1] !== undefined && (
<td className="dataGridChart__legendBoolean">
{chartData.data[1].key_as_string?.slice(0, 1) ?? ''}
</td>
)}
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -185,14 +193,16 @@ export const useColumnChart = (
// The if/else if/else is a work-around because `.map()` doesn't work with union types. // The if/else if/else is a work-around because `.map()` doesn't work with union types.
// See TS Caveats for details: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-3.html#caveats // See TS Caveats for details: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-3.html#caveats
if (isOrdinalChartData(chartData)) { if (isOrdinalChartData(chartData)) {
data = chartData.data.map((d: OrdinalDataItem) => ({ data = chartData.data.map((d: OrdinalDataItem, idx) => ({
...d, ...d,
x: idx,
key_as_string: d.key_as_string ?? d.key, key_as_string: d.key_as_string ?? d.key,
color: getColor(d), color: getColor(d),
})); }));
} else if (isNumericChartData(chartData)) { } else if (isNumericChartData(chartData)) {
data = chartData.data.map((d: NumericDataItem) => ({ data = chartData.data.map((d: NumericDataItem, idx) => ({
...d, ...d,
x: idx,
key_as_string: d.key_as_string || d.key, key_as_string: d.key_as_string || d.key,
color: getColor(d), color: getColor(d),
})); }));

View file

@ -75,14 +75,17 @@ export const MetricDistributionChart: FC<Props> = ({
return ( return (
<MetricDistributionChartTooltipHeader <MetricDistributionChartTooltipHeader
chartPoint={chartPoint} chartPoint={chartPoint}
maxWidth={width / 2} maxWidth={width}
fieldFormat={fieldFormat} fieldFormat={fieldFormat}
/> />
); );
}; };
return ( return (
<div data-test-subj="dataVisualizerFieldDataMetricDistributionChart"> <div
data-test-subj="dataVisualizerFieldDataMetricDistributionChart"
className="dataGridChart__histogram"
>
<Chart size={{ width, height }}> <Chart size={{ width, height }}>
<Settings theme={theme} tooltip={{ headerFormatter }} /> <Settings theme={theme} tooltip={{ headerFormatter }} />
<Axis <Axis

View file

@ -5,13 +5,12 @@
* 2.0. * 2.0.
*/ */
import React, { useMemo, useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import { import {
CENTER_ALIGNMENT, CENTER_ALIGNMENT,
EuiBasicTableColumn, EuiBasicTableColumn,
EuiButtonIcon, EuiButtonIcon,
EuiFlexItem,
EuiIcon, EuiIcon,
EuiInMemoryTable, EuiInMemoryTable,
EuiText, EuiText,
@ -19,13 +18,13 @@ import {
HorizontalAlignment, HorizontalAlignment,
LEFT_ALIGNMENT, LEFT_ALIGNMENT,
RIGHT_ALIGNMENT, RIGHT_ALIGNMENT,
EuiResizeObserver,
} from '@elastic/eui'; } from '@elastic/eui';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { EuiTableComputedColumnType } from '@elastic/eui/src/components/basic_table/table_types'; import { EuiTableComputedColumnType } from '@elastic/eui/src/components/basic_table/table_types';
import { throttle } from 'lodash';
import { JOB_FIELD_TYPES, JobFieldType, DataVisualizerTableState } from '../../../../../common'; import { JOB_FIELD_TYPES, JobFieldType, DataVisualizerTableState } from '../../../../../common';
import { FieldTypeIcon } from '../field_type_icon';
import { DocumentStat } from './components/field_data_row/document_stats'; import { DocumentStat } from './components/field_data_row/document_stats';
import { DistinctValues } from './components/field_data_row/distinct_values';
import { IndexBasedNumberContentPreview } from './components/field_data_row/number_content_preview'; import { IndexBasedNumberContentPreview } from './components/field_data_row/number_content_preview';
import { useTableSettings } from './use_table_settings'; import { useTableSettings } from './use_table_settings';
@ -37,6 +36,9 @@ import {
} from './types/field_vis_config'; } from './types/field_vis_config';
import { FileBasedNumberContentPreview } from '../field_data_row'; import { FileBasedNumberContentPreview } from '../field_data_row';
import { BooleanContentPreview } from './components/field_data_row'; import { BooleanContentPreview } from './components/field_data_row';
import { calculateTableColumnsDimensions } from './utils';
import { DistinctValues } from './components/field_data_row/distinct_values';
import { FieldTypeIcon } from '../field_type_icon';
const FIELD_NAME = 'fieldName'; const FIELD_NAME = 'fieldName';
@ -49,6 +51,9 @@ interface DataVisualizerTableProps<T> {
updatePageState: (update: DataVisualizerTableState) => void; updatePageState: (update: DataVisualizerTableState) => void;
getItemIdToExpandedRowMap: (itemIds: string[], items: T[]) => ItemIdToExpandedRowMap; getItemIdToExpandedRowMap: (itemIds: string[], items: T[]) => ItemIdToExpandedRowMap;
extendedColumns?: Array<EuiBasicTableColumn<T>>; extendedColumns?: Array<EuiBasicTableColumn<T>>;
showPreviewByDefault?: boolean;
/** Callback to receive any updates when table or page state is changed **/
onChange?: (update: Partial<DataVisualizerTableState>) => void;
} }
export const DataVisualizerTable = <T extends DataVisualizerTableItem>({ export const DataVisualizerTable = <T extends DataVisualizerTableItem>({
@ -57,23 +62,52 @@ export const DataVisualizerTable = <T extends DataVisualizerTableItem>({
updatePageState, updatePageState,
getItemIdToExpandedRowMap, getItemIdToExpandedRowMap,
extendedColumns, extendedColumns,
showPreviewByDefault,
onChange,
}: DataVisualizerTableProps<T>) => { }: DataVisualizerTableProps<T>) => {
const [expandedRowItemIds, setExpandedRowItemIds] = useState<string[]>([]); const [expandedRowItemIds, setExpandedRowItemIds] = useState<string[]>([]);
const [expandAll, toggleExpandAll] = useState<boolean>(false); const [expandAll, setExpandAll] = useState<boolean>(false);
const { onTableChange, pagination, sorting } = useTableSettings<T>( const { onTableChange, pagination, sorting } = useTableSettings<T>(
items, items,
pageState, pageState,
updatePageState updatePageState
); );
const showDistributions: boolean = const [showDistributions, setShowDistributions] = useState<boolean>(showPreviewByDefault ?? true);
('showDistributions' in pageState && pageState.showDistributions) ?? true; const [dimensions, setDimensions] = useState(calculateTableColumnsDimensions());
const toggleShowDistribution = () => { const [tableWidth, setTableWidth] = useState<number>(1400);
updatePageState({
...pageState, const toggleExpandAll = useCallback(
showDistributions: !showDistributions, (shouldExpandAll: boolean) => {
}); setExpandedRowItemIds(
}; shouldExpandAll
? // Update list of ids in expandedRowIds to include all
(items.map((item) => item.fieldName).filter((id) => id !== undefined) as string[])
: // Otherwise, reset list of ids in expandedRowIds
[]
);
setExpandAll(shouldExpandAll);
},
[items]
);
// eslint-disable-next-line react-hooks/exhaustive-deps
const resizeHandler = useCallback(
throttle((e: { width: number; height: number }) => {
// When window or table is resized,
// update the column widths and other settings accordingly
setTableWidth(e.width);
setDimensions(calculateTableColumnsDimensions(e.width));
}, 500),
[tableWidth]
);
const toggleShowDistribution = useCallback(() => {
setShowDistributions(!showDistributions);
if (onChange) {
onChange({ showDistributions: !showDistributions });
}
}, [onChange, showDistributions]);
function toggleDetails(item: DataVisualizerTableItem) { function toggleDetails(item: DataVisualizerTableItem) {
if (item.fieldName === undefined) return; if (item.fieldName === undefined) return;
@ -90,31 +124,32 @@ export const DataVisualizerTable = <T extends DataVisualizerTableItem>({
const columns = useMemo(() => { const columns = useMemo(() => {
const expanderColumn: EuiTableComputedColumnType<DataVisualizerTableItem> = { const expanderColumn: EuiTableComputedColumnType<DataVisualizerTableItem> = {
name: ( name:
<EuiButtonIcon dimensions.breakPoint !== 'xs' && dimensions.breakPoint !== 's' ? (
data-test-subj={`dataVisualizerToggleDetailsForAllRowsButton ${ <EuiButtonIcon
expandAll ? 'expanded' : 'collapsed' data-test-subj={`dataVisualizerToggleDetailsForAllRowsButton ${
}`} expandAll ? 'expanded' : 'collapsed'
onClick={() => toggleExpandAll(!expandAll)} }`}
aria-label={ onClick={() => toggleExpandAll(!expandAll)}
!expandAll aria-label={
? i18n.translate('xpack.dataVisualizer.dataGrid.expandDetailsForAllAriaLabel', { !expandAll
defaultMessage: 'Expand details for all fields', ? i18n.translate('xpack.dataVisualizer.dataGrid.expandDetailsForAllAriaLabel', {
}) defaultMessage: 'Expand details for all fields',
: i18n.translate('xpack.dataVisualizer.dataGrid.collapseDetailsForAllAriaLabel', { })
defaultMessage: 'Collapse details for all fields', : i18n.translate('xpack.dataVisualizer.dataGrid.collapseDetailsForAllAriaLabel', {
}) defaultMessage: 'Collapse details for all fields',
} })
iconType={expandAll ? 'arrowUp' : 'arrowDown'} }
/> iconType={expandAll ? 'arrowDown' : 'arrowRight'}
), />
) : null,
align: RIGHT_ALIGNMENT, align: RIGHT_ALIGNMENT,
width: '40px', width: dimensions.expander,
isExpander: true, isExpander: true,
render: (item: DataVisualizerTableItem) => { render: (item: DataVisualizerTableItem) => {
const displayName = item.displayName ?? item.fieldName; const displayName = item.displayName ?? item.fieldName;
if (item.fieldName === undefined) return null; if (item.fieldName === undefined) return null;
const direction = expandedRowItemIds.includes(item.fieldName) ? 'arrowUp' : 'arrowDown'; const direction = expandedRowItemIds.includes(item.fieldName) ? 'arrowDown' : 'arrowRight';
return ( return (
<EuiButtonIcon <EuiButtonIcon
data-test-subj={`dataVisualizerDetailsToggle-${item.fieldName}-${direction}`} data-test-subj={`dataVisualizerDetailsToggle-${item.fieldName}-${direction}`}
@ -147,7 +182,7 @@ export const DataVisualizerTable = <T extends DataVisualizerTableItem>({
render: (fieldType: JobFieldType) => { render: (fieldType: JobFieldType) => {
return <FieldTypeIcon type={fieldType} tooltipEnabled={true} needsAria={true} />; return <FieldTypeIcon type={fieldType} tooltipEnabled={true} needsAria={true} />;
}, },
width: '75px', width: dimensions.type,
sortable: true, sortable: true,
align: CENTER_ALIGNMENT as HorizontalAlignment, align: CENTER_ALIGNMENT as HorizontalAlignment,
'data-test-subj': 'dataVisualizerTableColumnType', 'data-test-subj': 'dataVisualizerTableColumnType',
@ -163,8 +198,8 @@ export const DataVisualizerTable = <T extends DataVisualizerTableItem>({
const displayName = item.displayName ?? item.fieldName; const displayName = item.displayName ?? item.fieldName;
return ( return (
<EuiText size="s"> <EuiText size="xs" data-test-subj={`dataVisualizerDisplayName-${item.fieldName}`}>
<b data-test-subj={`dataVisualizerDisplayName-${item.fieldName}`}>{displayName}</b> {displayName}
</EuiText> </EuiText>
); );
}, },
@ -177,56 +212,65 @@ export const DataVisualizerTable = <T extends DataVisualizerTableItem>({
defaultMessage: 'Documents (%)', defaultMessage: 'Documents (%)',
}), }),
render: (value: number | undefined, item: DataVisualizerTableItem) => ( render: (value: number | undefined, item: DataVisualizerTableItem) => (
<DocumentStat config={item} /> <DocumentStat config={item} showIcon={dimensions.showIcon} />
), ),
sortable: (item: DataVisualizerTableItem) => item?.stats?.count, sortable: (item: DataVisualizerTableItem) => item?.stats?.count,
align: LEFT_ALIGNMENT as HorizontalAlignment, align: LEFT_ALIGNMENT as HorizontalAlignment,
'data-test-subj': 'dataVisualizerTableColumnDocumentsCount', 'data-test-subj': 'dataVisualizerTableColumnDocumentsCount',
width: dimensions.docCount,
}, },
{ {
field: 'stats.cardinality', field: 'stats.cardinality',
name: i18n.translate('xpack.dataVisualizer.dataGrid.distinctValuesColumnName', { name: i18n.translate('xpack.dataVisualizer.dataGrid.distinctValuesColumnName', {
defaultMessage: 'Distinct values', defaultMessage: 'Distinct values',
}), }),
render: (cardinality?: number) => <DistinctValues cardinality={cardinality} />, render: (cardinality: number | undefined) => (
<DistinctValues cardinality={cardinality} showIcon={dimensions.showIcon} />
),
sortable: true, sortable: true,
align: LEFT_ALIGNMENT as HorizontalAlignment, align: LEFT_ALIGNMENT as HorizontalAlignment,
'data-test-subj': 'dataVisualizerTableColumnDistinctValues', 'data-test-subj': 'dataVisualizerTableColumnDistinctValues',
width: dimensions.distinctValues,
}, },
{ {
name: ( name: (
<div style={{ display: 'flex', alignItems: 'center' }}> <div className={'columnHeader__title'}>
<EuiIcon type={'visBarVertical'} style={{ paddingRight: 4 }} /> {dimensions.showIcon ? (
<EuiIcon type={'visBarVertical'} className={'columnHeader__icon'} />
) : null}
{i18n.translate('xpack.dataVisualizer.dataGrid.distributionsColumnName', { {i18n.translate('xpack.dataVisualizer.dataGrid.distributionsColumnName', {
defaultMessage: 'Distributions', defaultMessage: 'Distributions',
})} })}
<EuiToolTip {
content={ <EuiToolTip
!showDistributions content={
? i18n.translate('xpack.dataVisualizer.dataGrid.showDistributionsTooltip', {
defaultMessage: 'Show distributions',
})
: i18n.translate('xpack.dataVisualizer.dataGrid.hideDistributionsTooltip', {
defaultMessage: 'Hide distributions',
})
}
>
<EuiButtonIcon
style={{ marginLeft: 4 }}
size={'s'}
iconType={showDistributions ? 'eye' : 'eyeClosed'}
onClick={() => toggleShowDistribution()}
aria-label={
!showDistributions !showDistributions
? i18n.translate('xpack.dataVisualizer.dataGrid.showDistributionsAriaLabel', { ? i18n.translate('xpack.dataVisualizer.dataGrid.showDistributionsTooltip', {
defaultMessage: 'Show distributions', defaultMessage: 'Show distributions',
}) })
: i18n.translate('xpack.dataVisualizer.dataGrid.hideDistributionsAriaLabel', { : i18n.translate('xpack.dataVisualizer.dataGrid.hideDistributionsTooltip', {
defaultMessage: 'Hide distributions', defaultMessage: 'Hide distributions',
}) })
} }
/> >
</EuiToolTip> <EuiButtonIcon
style={{ marginLeft: 4 }}
size={'s'}
iconType={!showDistributions ? 'eye' : 'eyeClosed'}
onClick={() => toggleShowDistribution()}
aria-label={
showDistributions
? i18n.translate('xpack.dataVisualizer.dataGrid.showDistributionsAriaLabel', {
defaultMessage: 'Show distributions',
})
: i18n.translate('xpack.dataVisualizer.dataGrid.hideDistributionsAriaLabel', {
defaultMessage: 'Hide distributions',
})
}
/>
</EuiToolTip>
}
</div> </div>
), ),
render: (item: DataVisualizerTableItem) => { render: (item: DataVisualizerTableItem) => {
@ -252,41 +296,49 @@ export const DataVisualizerTable = <T extends DataVisualizerTableItem>({
return null; return null;
}, },
width: dimensions.distributions,
align: LEFT_ALIGNMENT as HorizontalAlignment, align: LEFT_ALIGNMENT as HorizontalAlignment,
'data-test-subj': 'dataVisualizerTableColumnDistribution', 'data-test-subj': 'dataVisualizerTableColumnDistribution',
}, },
]; ];
return extendedColumns ? [...baseColumns, ...extendedColumns] : baseColumns; return extendedColumns ? [...baseColumns, ...extendedColumns] : baseColumns;
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [expandAll, showDistributions, updatePageState, extendedColumns]); }, [
expandAll,
showDistributions,
updatePageState,
extendedColumns,
dimensions.breakPoint,
toggleExpandAll,
]);
const itemIdToExpandedRowMap = useMemo(() => { const itemIdToExpandedRowMap = useMemo(() => {
let itemIds = expandedRowItemIds; const itemIds = expandedRowItemIds;
if (expandAll) {
itemIds = items.map((i) => i[FIELD_NAME]).filter((f) => f !== undefined) as string[];
}
return getItemIdToExpandedRowMap(itemIds, items); return getItemIdToExpandedRowMap(itemIds, items);
// eslint-disable-next-line react-hooks/exhaustive-deps }, [items, expandedRowItemIds, getItemIdToExpandedRowMap]);
}, [expandAll, items, expandedRowItemIds]);
return ( return (
<EuiFlexItem data-test-subj="dataVisualizerTableContainer"> <EuiResizeObserver onResize={resizeHandler}>
<EuiInMemoryTable<T> {(resizeRef) => (
className={'dataVisualizer'} <div data-test-subj="dataVisualizerTableContainer" ref={resizeRef}>
items={items} <EuiInMemoryTable<T>
itemId={FIELD_NAME} className={'dvTable'}
columns={columns} items={items}
pagination={pagination} itemId={FIELD_NAME}
sorting={sorting} columns={columns}
isExpandable={true} pagination={pagination}
itemIdToExpandedRowMap={itemIdToExpandedRowMap} sorting={sorting}
isSelectable={false} isExpandable={true}
onTableChange={onTableChange} itemIdToExpandedRowMap={itemIdToExpandedRowMap}
data-test-subj={'dataVisualizerTable'} isSelectable={false}
rowProps={(item) => ({ onTableChange={onTableChange}
'data-test-subj': `dataVisualizerRow row-${item.fieldName}`, data-test-subj={'dataVisualizerTable'}
})} rowProps={(item) => ({
/> 'data-test-subj': `dataVisualizerRow row-${item.fieldName}`,
</EuiFlexItem> })}
/>
</div>
)}
</EuiResizeObserver>
); );
}; };

View file

@ -6,7 +6,9 @@
*/ */
import type { FieldVisConfig, FileBasedFieldVisConfig } from './field_vis_config'; import type { FieldVisConfig, FileBasedFieldVisConfig } from './field_vis_config';
import { IndexPatternField } from '../../../../../../../../../src/plugins/data/common';
export interface FieldDataRowProps { export interface FieldDataRowProps {
config: FieldVisConfig | FileBasedFieldVisConfig; config: FieldVisConfig | FileBasedFieldVisConfig;
onAddFilter?: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
} }

View file

@ -10,7 +10,7 @@ import { useCallback, useMemo } from 'react';
import { DataVisualizerTableState } from '../../../../../common'; import { DataVisualizerTableState } from '../../../../../common';
const PAGE_SIZE_OPTIONS = [10, 25, 50]; const PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
interface UseTableSettingsReturnValue<T> { interface UseTableSettingsReturnValue<T> {
onTableChange: EuiBasicTableProps<T>['onChange']; onTableChange: EuiBasicTableProps<T>['onChange'];

View file

@ -5,6 +5,7 @@
* 2.0. * 2.0.
*/ */
import { getBreakpoint } from '@elastic/eui';
import { FileBasedFieldVisConfig } from './types'; import { FileBasedFieldVisConfig } from './types';
export const getTFPercentage = (config: FileBasedFieldVisConfig) => { export const getTFPercentage = (config: FileBasedFieldVisConfig) => {
@ -36,3 +37,45 @@ export const getTFPercentage = (config: FileBasedFieldVisConfig) => {
falseCount, falseCount,
}; };
}; };
export const calculateTableColumnsDimensions = (width?: number) => {
const defaultSettings = {
expander: '40px',
type: '75px',
docCount: '225px',
distinctValues: '225px',
distributions: '225px',
showIcon: true,
breakPoint: 'xl',
};
if (width === undefined) return defaultSettings;
const breakPoint = getBreakpoint(width);
switch (breakPoint) {
case 'xs':
case 's':
return {
expander: '25px',
type: '40px',
docCount: 'auto',
distinctValues: 'auto',
distributions: 'auto',
showIcon: false,
breakPoint,
};
case 'm':
case 'l':
return {
expander: '25px',
type: '40px',
docCount: 'auto',
distinctValues: 'auto',
distributions: 'auto',
showIcon: false,
breakPoint,
};
default:
return defaultSettings;
}
};

View file

@ -4,16 +4,4 @@
.topValuesValueLabelContainer { .topValuesValueLabelContainer {
margin-right: $euiSizeM; margin-right: $euiSizeM;
&.topValuesValueLabelContainer--small {
width:70px;
}
&.topValuesValueLabelContainer--large {
width: 200px;
}
}
.topValuesPercentLabelContainer {
margin-left: $euiSizeM;
width:70px;
} }

View file

@ -12,21 +12,25 @@ import {
EuiProgress, EuiProgress,
EuiSpacer, EuiSpacer,
EuiText, EuiText,
EuiToolTip, EuiButtonIcon,
} from '@elastic/eui'; } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react'; import { FormattedMessage } from '@kbn/i18n/react';
import classNames from 'classnames'; import classNames from 'classnames';
import { i18n } from '@kbn/i18n';
import { roundToDecimalPlace, kibanaFieldFormat } from '../utils'; import { roundToDecimalPlace, kibanaFieldFormat } from '../utils';
import { ExpandedRowFieldHeader } from '../stats_table/components/expanded_row_field_header'; import { ExpandedRowFieldHeader } from '../stats_table/components/expanded_row_field_header';
import { FieldVisStats } from '../../../../../common/types'; import { FieldVisStats } from '../../../../../common/types';
import { ExpandedRowPanel } from '../stats_table/components/field_data_expanded_row/expanded_row_panel';
import { IndexPatternField } from '../../../../../../../../src/plugins/data/common/data_views/fields';
interface Props { interface Props {
stats: FieldVisStats | undefined; stats: FieldVisStats | undefined;
fieldFormat?: any; fieldFormat?: any;
barColor?: 'primary' | 'secondary' | 'danger' | 'subdued' | 'accent'; barColor?: 'primary' | 'secondary' | 'danger' | 'subdued' | 'accent';
compressed?: boolean; compressed?: boolean;
onAddFilter?: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
} }
function getPercentLabel(docCount: number, topValuesSampleSize: number): string { function getPercentLabel(docCount: number, topValuesSampleSize: number): string {
@ -38,13 +42,23 @@ function getPercentLabel(docCount: number, topValuesSampleSize: number): string
} }
} }
export const TopValues: FC<Props> = ({ stats, fieldFormat, barColor, compressed }) => { export const TopValues: FC<Props> = ({ stats, fieldFormat, barColor, compressed, onAddFilter }) => {
if (stats === undefined) return null; if (stats === undefined) return null;
const { topValues, topValuesSampleSize, topValuesSamplerShardSize, count, isTopValuesSampled } = const {
stats; topValues,
topValuesSampleSize,
topValuesSamplerShardSize,
count,
isTopValuesSampled,
fieldName,
} = stats;
const progressBarMax = isTopValuesSampled === true ? topValuesSampleSize : count; const progressBarMax = isTopValuesSampled === true ? topValuesSampleSize : count;
return ( return (
<EuiFlexItem data-test-subj={'dataVisualizerFieldDataTopValues'}> <ExpandedRowPanel
dataTestSubj={'dataVisualizerFieldDataTopValues'}
className={classNames('dvPanel__wrapper', compressed ? 'dvPanel--compressed' : undefined)}
>
<ExpandedRowFieldHeader> <ExpandedRowFieldHeader>
<FormattedMessage <FormattedMessage
id="xpack.dataVisualizer.dataGrid.field.topValuesLabel" id="xpack.dataVisualizer.dataGrid.field.topValuesLabel"
@ -54,49 +68,90 @@ export const TopValues: FC<Props> = ({ stats, fieldFormat, barColor, compressed
<div <div
data-test-subj="dataVisualizerFieldDataTopValuesContent" data-test-subj="dataVisualizerFieldDataTopValuesContent"
className={'fieldDataTopValuesContainer'} className={classNames('fieldDataTopValuesContainer', 'dvTopValues__wrapper')}
> >
{Array.isArray(topValues) && {Array.isArray(topValues) &&
topValues.map((value) => ( topValues.map((value) => (
<EuiFlexGroup gutterSize="xs" alignItems="center" key={value.key}> <EuiFlexGroup gutterSize="xs" alignItems="center" key={value.key}>
<EuiFlexItem
grow={false}
className={classNames(
'eui-textTruncate',
'topValuesValueLabelContainer',
`topValuesValueLabelContainer--${compressed === true ? 'small' : 'large'}`
)}
>
<EuiToolTip content={kibanaFieldFormat(value.key, fieldFormat)} position="right">
<EuiText size="xs" textAlign={'right'} color="subdued">
{kibanaFieldFormat(value.key, fieldFormat)}
</EuiText>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem data-test-subj="dataVisualizerFieldDataTopValueBar"> <EuiFlexItem data-test-subj="dataVisualizerFieldDataTopValueBar">
<EuiProgress <EuiProgress
value={value.doc_count} value={value.doc_count}
max={progressBarMax} max={progressBarMax}
color={barColor} color={barColor}
size="m" size="xs"
label={kibanaFieldFormat(value.key, fieldFormat)}
className={classNames('eui-textTruncate', 'topValuesValueLabelContainer')}
valueText={
progressBarMax !== undefined
? getPercentLabel(value.doc_count, progressBarMax)
: undefined
}
/> />
</EuiFlexItem> </EuiFlexItem>
{progressBarMax !== undefined && ( {fieldName !== undefined && value.key !== undefined && onAddFilter !== undefined ? (
<EuiFlexItem <>
grow={false} <EuiButtonIcon
className={classNames('eui-textTruncate', 'topValuesPercentLabelContainer')} iconSize="s"
> iconType="plusInCircle"
<EuiText size="xs" textAlign="left" color="subdued"> onClick={() =>
{getPercentLabel(value.doc_count, progressBarMax)} onAddFilter(
</EuiText> fieldName,
</EuiFlexItem> typeof value.key === 'number' ? value.key.toString() : value.key,
)} '+'
)
}
aria-label={i18n.translate(
'xpack.dataVisualizer.dataGrid.field.addFilterAriaLabel',
{
defaultMessage: 'Filter for {fieldName}: "{value}"',
values: { fieldName, value: value.key },
}
)}
data-test-subj={`dvFieldDataTopValuesAddFilterButton-${value.key}-${value.key}`}
style={{
minHeight: 'auto',
minWidth: 'auto',
paddingRight: 2,
paddingLeft: 2,
paddingTop: 0,
paddingBottom: 0,
}}
/>
<EuiButtonIcon
iconSize="s"
iconType="minusInCircle"
onClick={() =>
onAddFilter(
fieldName,
typeof value.key === 'number' ? value.key.toString() : value.key,
'-'
)
}
aria-label={i18n.translate(
'xpack.dataVisualizer.dataGrid.field.removeFilterAriaLabel',
{
defaultMessage: 'Filter out {fieldName}: "{value}"',
values: { fieldName, value: value.key },
}
)}
data-test-subj={`dvFieldDataTopValuesExcludeFilterButton-${value.key}-${value.key}`}
style={{
minHeight: 'auto',
minWidth: 'auto',
paddingTop: 0,
paddingBottom: 0,
paddingRight: 2,
paddingLeft: 2,
}}
/>
</>
) : null}
</EuiFlexGroup> </EuiFlexGroup>
))} ))}
{isTopValuesSampled === true && ( {isTopValuesSampled === true && (
<Fragment> <Fragment>
<EuiSpacer size="xs" /> <EuiSpacer size="xs" />
<EuiText size="xs" textAlign={'left'}> <EuiText size="xs" textAlign={'center'}>
<FormattedMessage <FormattedMessage
id="xpack.dataVisualizer.dataGrid.field.topValues.calculatedFromSampleDescription" id="xpack.dataVisualizer.dataGrid.field.topValues.calculatedFromSampleDescription"
defaultMessage="Calculated from sample of {topValuesSamplerShardSize} documents per shard" defaultMessage="Calculated from sample of {topValuesSamplerShardSize} documents per shard"
@ -108,6 +163,6 @@ export const TopValues: FC<Props> = ({ stats, fieldFormat, barColor, compressed
</Fragment> </Fragment>
)} )}
</div> </div>
</EuiFlexItem> </ExpandedRowPanel>
); );
}; };

View file

@ -23,6 +23,9 @@ export const jobTypeAriaLabels = {
geoPointParam: 'geo point', geoPointParam: 'geo point',
}, },
}), }),
GEO_SHAPE: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.geoShapeTypeAriaLabel', {
defaultMessage: 'geo shape type',
}),
IP: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.ipTypeAriaLabel', { IP: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.ipTypeAriaLabel', {
defaultMessage: 'ip type', defaultMessage: 'ip type',
}), }),
@ -32,6 +35,9 @@ export const jobTypeAriaLabels = {
NUMBER: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.numberTypeAriaLabel', { NUMBER: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.numberTypeAriaLabel', {
defaultMessage: 'number type', defaultMessage: 'number type',
}), }),
HISTOGRAM: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.histogramTypeAriaLabel', {
defaultMessage: 'histogram type',
}),
TEXT: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.textTypeAriaLabel', { TEXT: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.textTypeAriaLabel', {
defaultMessage: 'text type', defaultMessage: 'text type',
}), }),
@ -40,6 +46,48 @@ export const jobTypeAriaLabels = {
}), }),
}; };
export const jobTypeLabels = {
[JOB_FIELD_TYPES.BOOLEAN]: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.booleanTypeLabel', {
defaultMessage: 'Boolean',
}),
[JOB_FIELD_TYPES.DATE]: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.dateTypeLabel', {
defaultMessage: 'Date',
}),
[JOB_FIELD_TYPES.GEO_POINT]: i18n.translate(
'xpack.dataVisualizer.fieldTypeIcon.geoPointTypeLabel',
{
defaultMessage: 'Geo point',
}
),
[JOB_FIELD_TYPES.GEO_SHAPE]: i18n.translate(
'xpack.dataVisualizer.fieldTypeIcon.geoShapeTypeLabel',
{
defaultMessage: 'Geo shape',
}
),
[JOB_FIELD_TYPES.IP]: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.ipTypeLabel', {
defaultMessage: 'IP',
}),
[JOB_FIELD_TYPES.KEYWORD]: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.keywordTypeLabel', {
defaultMessage: 'Keyword',
}),
[JOB_FIELD_TYPES.NUMBER]: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.numberTypeLabel', {
defaultMessage: 'Number',
}),
[JOB_FIELD_TYPES.HISTOGRAM]: i18n.translate(
'xpack.dataVisualizer.fieldTypeIcon.histogramTypeLabel',
{
defaultMessage: 'Histogram',
}
),
[JOB_FIELD_TYPES.TEXT]: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.textTypeLabel', {
defaultMessage: 'Text',
}),
[JOB_FIELD_TYPES.UNKNOWN]: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.unknownTypeLabel', {
defaultMessage: 'Unknown',
}),
};
export const getJobTypeAriaLabel = (type: string) => { export const getJobTypeAriaLabel = (type: string) => {
const requestedFieldType = Object.keys(JOB_FIELD_TYPES).find( const requestedFieldType = Object.keys(JOB_FIELD_TYPES).find(
(k) => JOB_FIELD_TYPES[k as keyof typeof JOB_FIELD_TYPES] === type (k) => JOB_FIELD_TYPES[k as keyof typeof JOB_FIELD_TYPES] === type

View file

@ -40,6 +40,7 @@ export const ActionsPanel: FC<Props> = ({
const { const {
services: { services: {
data,
application: { capabilities }, application: { capabilities },
share: { share: {
urlGenerators: { getUrlGenerator }, urlGenerators: { getUrlGenerator },
@ -60,6 +61,9 @@ export const ActionsPanel: FC<Props> = ({
const state: DiscoverUrlGeneratorState = { const state: DiscoverUrlGeneratorState = {
indexPatternId, indexPatternId,
}; };
state.filters = data.query.filterManager.getFilters() ?? [];
if (searchString && searchQueryLanguage !== undefined) { if (searchString && searchQueryLanguage !== undefined) {
state.query = { query: searchString, language: searchQueryLanguage }; state.query = { query: searchString, language: searchQueryLanguage };
} }
@ -113,6 +117,7 @@ export const ActionsPanel: FC<Props> = ({
capabilities, capabilities,
getUrlGenerator, getUrlGenerator,
additionalLinks, additionalLinks,
data.query,
]); ]);
// Note we use display:none for the DataRecognizer section as it needs to be // Note we use display:none for the DataRecognizer section as it needs to be

View file

@ -0,0 +1,13 @@
.dataViewTitleHeader {
min-width: 300px;
display: flex;
flex-direction: row;
align-items: center;
}
@include euiBreakpoint('xs', 's', 'm', 'l') {
.dataVisualizerPageHeader {
flex-direction: column;
align-items: flex-start;
}
}

View file

@ -23,12 +23,12 @@ import { EuiTableActionsColumnType } from '@elastic/eui/src/components/basic_tab
import { FormattedMessage } from '@kbn/i18n/react'; import { FormattedMessage } from '@kbn/i18n/react';
import { Required } from 'utility-types'; import { Required } from 'utility-types';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { Filter } from '@kbn/es-query';
import { import {
IndexPatternField,
KBN_FIELD_TYPES, KBN_FIELD_TYPES,
UI_SETTINGS, UI_SETTINGS,
Query, Query,
IndexPattern, generateFilters,
} from '../../../../../../../../src/plugins/data/public'; } from '../../../../../../../../src/plugins/data/public';
import { FullTimeRangeSelector } from '../full_time_range_selector'; import { FullTimeRangeSelector } from '../full_time_range_selector';
import { usePageUrlState, useUrlState } from '../../../common/util/url_state'; import { usePageUrlState, useUrlState } from '../../../common/util/url_state';
@ -65,10 +65,12 @@ import { DatePickerWrapper } from '../../../common/components/date_picker_wrappe
import { dataVisualizerRefresh$ } from '../../services/timefilter_refresh_service'; import { dataVisualizerRefresh$ } from '../../services/timefilter_refresh_service';
import { HelpMenu } from '../../../common/components/help_menu'; import { HelpMenu } from '../../../common/components/help_menu';
import { TimeBuckets } from '../../services/time_buckets'; import { TimeBuckets } from '../../services/time_buckets';
import { extractSearchData } from '../../utils/saved_search_utils'; import { createMergedEsQuery, getEsQueryFromSavedSearch } from '../../utils/saved_search_utils';
import { DataVisualizerIndexPatternManagement } from '../index_pattern_management'; import { DataVisualizerIndexPatternManagement } from '../index_pattern_management';
import { ResultLink } from '../../../common/components/results_links'; import { ResultLink } from '../../../common/components/results_links';
import { extractErrorProperties } from '../../utils/error_utils'; import { extractErrorProperties } from '../../utils/error_utils';
import { IndexPatternField, IndexPattern } from '../../../../../../../../src/plugins/data/common';
import './_index.scss';
interface DataVisualizerPageState { interface DataVisualizerPageState {
overallStats: OverallStats; overallStats: OverallStats;
@ -85,7 +87,7 @@ const defaultSearchQuery = {
match_all: {}, match_all: {},
}; };
function getDefaultPageState(): DataVisualizerPageState { export function getDefaultPageState(): DataVisualizerPageState {
return { return {
overallStats: { overallStats: {
totalCount: 0, totalCount: 0,
@ -103,22 +105,25 @@ function getDefaultPageState(): DataVisualizerPageState {
documentCountStats: undefined, documentCountStats: undefined,
}; };
} }
export const getDefaultDataVisualizerListState = export const getDefaultDataVisualizerListState = (
(): Required<DataVisualizerIndexBasedAppState> => ({ overrides?: Partial<DataVisualizerIndexBasedAppState>
pageIndex: 0, ): Required<DataVisualizerIndexBasedAppState> => ({
pageSize: 10, pageIndex: 0,
sortField: 'fieldName', pageSize: 25,
sortDirection: 'asc', sortField: 'fieldName',
visibleFieldTypes: [], sortDirection: 'asc',
visibleFieldNames: [], visibleFieldTypes: [],
samplerShardSize: 5000, visibleFieldNames: [],
searchString: '', samplerShardSize: 5000,
searchQuery: defaultSearchQuery, searchString: '',
searchQueryLanguage: SEARCH_QUERY_LANGUAGE.KUERY, searchQuery: defaultSearchQuery,
showDistributions: true, searchQueryLanguage: SEARCH_QUERY_LANGUAGE.KUERY,
showAllFields: false, filters: [],
showEmptyFields: false, showDistributions: true,
}); showAllFields: false,
showEmptyFields: false,
...overrides,
});
export interface IndexDataVisualizerViewProps { export interface IndexDataVisualizerViewProps {
currentIndexPattern: IndexPattern; currentIndexPattern: IndexPattern;
@ -129,7 +134,7 @@ const restorableDefaults = getDefaultDataVisualizerListState();
export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVisualizerProps) => { export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVisualizerProps) => {
const { services } = useDataVisualizerKibana(); const { services } = useDataVisualizerKibana();
const { docLinks, notifications, uiSettings } = services; const { docLinks, notifications, uiSettings, data } = services;
const { toasts } = notifications; const { toasts } = notifications;
const [dataVisualizerListState, setDataVisualizerListState] = usePageUrlState( const [dataVisualizerListState, setDataVisualizerListState] = usePageUrlState(
@ -150,6 +155,15 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
} }
}, [dataVisualizerProps?.currentSavedSearch]); }, [dataVisualizerProps?.currentSavedSearch]);
useEffect(() => {
return () => {
// When navigating away from the index pattern
// Reset all previously set filters
// to make sure new page doesn't have unrelated filters
data.query.filterManager.removeAll();
};
}, [currentIndexPattern.id, data.query.filterManager]);
const getTimeBuckets = useCallback(() => { const getTimeBuckets = useCallback(() => {
return new TimeBuckets({ return new TimeBuckets({
[UI_SETTINGS.HISTOGRAM_MAX_BARS]: uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS), [UI_SETTINGS.HISTOGRAM_MAX_BARS]: uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS),
@ -227,13 +241,17 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
const defaults = getDefaultPageState(); const defaults = getDefaultPageState();
const { searchQueryLanguage, searchString, searchQuery } = useMemo(() => { const { searchQueryLanguage, searchString, searchQuery } = useMemo(() => {
const searchData = extractSearchData( const searchData = getEsQueryFromSavedSearch({
currentSavedSearch, indexPattern: currentIndexPattern,
currentIndexPattern, uiSettings,
uiSettings.get(UI_SETTINGS.QUERY_STRING_OPTIONS) savedSearch: currentSavedSearch,
); filterManager: data.query.filterManager,
});
if (searchData === undefined || dataVisualizerListState.searchString !== '') { if (searchData === undefined || dataVisualizerListState.searchString !== '') {
if (dataVisualizerListState.filters) {
data.query.filterManager.setFilters(dataVisualizerListState.filters);
}
return { return {
searchQuery: dataVisualizerListState.searchQuery, searchQuery: dataVisualizerListState.searchQuery,
searchString: dataVisualizerListState.searchString, searchString: dataVisualizerListState.searchString,
@ -247,26 +265,31 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
}; };
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentSavedSearch, currentIndexPattern, dataVisualizerListState]); }, [currentSavedSearch, currentIndexPattern, dataVisualizerListState, data.query]);
const setSearchParams = (searchParams: { const setSearchParams = useCallback(
searchQuery: Query['query']; (searchParams: {
searchString: Query['query']; searchQuery: Query['query'];
queryLanguage: SearchQueryLanguage; searchString: Query['query'];
}) => { queryLanguage: SearchQueryLanguage;
// When the user loads saved search and then clear or modify the query filters: Filter[];
// we should remove the saved search and replace it with the index pattern id }) => {
if (currentSavedSearch !== null) { // When the user loads saved search and then clear or modify the query
setCurrentSavedSearch(null); // we should remove the saved search and replace it with the index pattern id
} if (currentSavedSearch !== null) {
setCurrentSavedSearch(null);
}
setDataVisualizerListState({ setDataVisualizerListState({
...dataVisualizerListState, ...dataVisualizerListState,
searchQuery: searchParams.searchQuery, searchQuery: searchParams.searchQuery,
searchString: searchParams.searchString, searchString: searchParams.searchString,
searchQueryLanguage: searchParams.queryLanguage, searchQueryLanguage: searchParams.queryLanguage,
}); filters: searchParams.filters,
}; });
},
[currentSavedSearch, dataVisualizerListState, setDataVisualizerListState]
);
const samplerShardSize = const samplerShardSize =
dataVisualizerListState.samplerShardSize ?? restorableDefaults.samplerShardSize; dataVisualizerListState.samplerShardSize ?? restorableDefaults.samplerShardSize;
@ -305,6 +328,52 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
const [nonMetricConfigs, setNonMetricConfigs] = useState(defaults.nonMetricConfigs); const [nonMetricConfigs, setNonMetricConfigs] = useState(defaults.nonMetricConfigs);
const [nonMetricsLoaded, setNonMetricsLoaded] = useState(defaults.nonMetricsLoaded); const [nonMetricsLoaded, setNonMetricsLoaded] = useState(defaults.nonMetricsLoaded);
const onAddFilter = useCallback(
(field: IndexPatternField | string, values: string, operation: '+' | '-') => {
const newFilters = generateFilters(
data.query.filterManager,
field,
values,
operation,
String(currentIndexPattern.id)
);
if (newFilters) {
data.query.filterManager.addFilters(newFilters);
}
// Merge current query with new filters
const mergedQuery = {
query: searchString || '',
language: searchQueryLanguage,
};
const combinedQuery = createMergedEsQuery(
{
query: searchString || '',
language: searchQueryLanguage,
},
data.query.filterManager.getFilters() ?? [],
currentIndexPattern,
uiSettings
);
setSearchParams({
searchQuery: combinedQuery,
searchString: mergedQuery.query,
queryLanguage: mergedQuery.language as SearchQueryLanguage,
filters: data.query.filterManager.getFilters(),
});
},
[
currentIndexPattern,
data.query.filterManager,
searchQueryLanguage,
searchString,
setSearchParams,
uiSettings,
]
);
useEffect(() => { useEffect(() => {
const timeUpdateSubscription = merge( const timeUpdateSubscription = merge(
timefilter.getTimeUpdate$(), timefilter.getTimeUpdate$(),
@ -666,11 +735,11 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
const fieldData = nonMetricFieldData.find((f) => f.fieldName === field.spec.name); const fieldData = nonMetricFieldData.find((f) => f.fieldName === field.spec.name);
const nonMetricConfig = { const nonMetricConfig = {
...fieldData, ...(fieldData ? fieldData : {}),
fieldFormat: currentIndexPattern.getFormatterForField(field), fieldFormat: currentIndexPattern.getFormatterForField(field),
aggregatable: field.aggregatable, aggregatable: field.aggregatable,
scripted: field.scripted, scripted: field.scripted,
loading: fieldData.existsInDocs, loading: fieldData?.existsInDocs,
deletable: field.runtimeField !== undefined, deletable: field.runtimeField !== undefined,
}; };
@ -751,13 +820,14 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
item={item} item={item}
indexPattern={currentIndexPattern} indexPattern={currentIndexPattern}
combinedQuery={{ searchQueryLanguage, searchString }} combinedQuery={{ searchQueryLanguage, searchString }}
onAddFilter={onAddFilter}
/> />
); );
} }
return m; return m;
}, {} as ItemIdToExpandedRowMap); }, {} as ItemIdToExpandedRowMap);
}, },
[currentIndexPattern, searchQueryLanguage, searchString] [currentIndexPattern, searchQueryLanguage, searchString, onAddFilter]
); );
// Some actions open up fly-out or popup // Some actions open up fly-out or popup
@ -809,17 +879,10 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
<EuiPageBody> <EuiPageBody>
<EuiFlexGroup gutterSize="m"> <EuiFlexGroup gutterSize="m">
<EuiFlexItem> <EuiFlexItem>
<EuiPageContentHeader> <EuiPageContentHeader className="dataVisualizerPageHeader">
<EuiPageContentHeaderSection> <EuiPageContentHeaderSection>
<div <div className="dataViewTitleHeader">
style={{ <EuiTitle>
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
}}
>
<EuiTitle size="l">
<h1>{currentIndexPattern.title}</h1> <h1>{currentIndexPattern.title}</h1>
</EuiTitle> </EuiTitle>
<DataVisualizerIndexPatternManagement <DataVisualizerIndexPatternManagement
@ -829,23 +892,26 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
</div> </div>
</EuiPageContentHeaderSection> </EuiPageContentHeaderSection>
<EuiPageContentHeaderSection data-test-subj="dataVisualizerTimeRangeSelectorSection"> <EuiFlexGroup
<EuiFlexGroup alignItems="center" justifyContent="flexEnd" gutterSize="s"> alignItems="center"
{currentIndexPattern.timeFieldName !== undefined && ( justifyContent="flexEnd"
<EuiFlexItem grow={false}> gutterSize="s"
<FullTimeRangeSelector data-test-subj="dataVisualizerTimeRangeSelectorSection"
indexPattern={currentIndexPattern} >
query={undefined} {currentIndexPattern.timeFieldName !== undefined && (
disabled={false}
timefilter={timefilter}
/>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<DatePickerWrapper /> <FullTimeRangeSelector
indexPattern={currentIndexPattern}
query={undefined}
disabled={false}
timefilter={timefilter}
/>
</EuiFlexItem> </EuiFlexItem>
</EuiFlexGroup> )}
</EuiPageContentHeaderSection> <EuiFlexItem grow={false}>
<DatePickerWrapper />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageContentHeader> </EuiPageContentHeader>
</EuiFlexItem> </EuiFlexItem>
</EuiFlexGroup> </EuiFlexGroup>
@ -862,8 +928,6 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
/> />
</EuiFlexItem> </EuiFlexItem>
)} )}
<EuiSpacer size={'m'} />
<SearchPanel <SearchPanel
indexPattern={currentIndexPattern} indexPattern={currentIndexPattern}
searchString={searchString} searchString={searchString}
@ -879,8 +943,9 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
visibleFieldNames={visibleFieldNames} visibleFieldNames={visibleFieldNames}
setVisibleFieldNames={setVisibleFieldNames} setVisibleFieldNames={setVisibleFieldNames}
showEmptyFields={showEmptyFields} showEmptyFields={showEmptyFields}
onAddFilter={onAddFilter}
/> />
<EuiSpacer size={'l'} /> <EuiSpacer size={'m'} />
<FieldCountPanel <FieldCountPanel
showEmptyFields={showEmptyFields} showEmptyFields={showEmptyFields}
toggleShowEmptyFields={toggleShowEmptyFields} toggleShowEmptyFields={toggleShowEmptyFields}

View file

@ -8,32 +8,28 @@
import React, { FC, useMemo } from 'react'; import React, { FC, useMemo } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { JOB_FIELD_TYPES_OPTIONS, JobFieldType } from '../../../../../common'; import { JobFieldType } from '../../../../../common';
import { FieldTypeIcon } from '../../../common/components/field_type_icon'; import { FieldTypeIcon } from '../../../common/components/field_type_icon';
import { MultiSelectPicker, Option } from '../../../common/components/multi_select_picker'; import { MultiSelectPicker, Option } from '../../../common/components/multi_select_picker';
import { jobTypeLabels } from '../../../common/util/field_types_utils';
export const DatavisualizerFieldTypeFilter: FC<{ export const DataVisualizerFieldTypeFilter: FC<{
indexedFieldTypes: JobFieldType[]; indexedFieldTypes: JobFieldType[];
setVisibleFieldTypes(q: string[]): void; setVisibleFieldTypes(q: string[]): void;
visibleFieldTypes: string[]; visibleFieldTypes: string[];
}> = ({ indexedFieldTypes, setVisibleFieldTypes, visibleFieldTypes }) => { }> = ({ indexedFieldTypes, setVisibleFieldTypes, visibleFieldTypes }) => {
const options: Option[] = useMemo(() => { const options: Option[] = useMemo(() => {
return indexedFieldTypes.map((indexedFieldName) => { return indexedFieldTypes.map((indexedFieldName) => {
const item = JOB_FIELD_TYPES_OPTIONS[indexedFieldName]; const label = jobTypeLabels[indexedFieldName] ?? '';
return { return {
value: indexedFieldName, value: indexedFieldName,
name: ( name: (
<EuiFlexGroup> <EuiFlexGroup>
<EuiFlexItem grow={true}> {item.name}</EuiFlexItem> <EuiFlexItem grow={true}> {label}</EuiFlexItem>
{indexedFieldName && ( {indexedFieldName && (
<EuiFlexItem grow={false}> <EuiFlexItem grow={false}>
<FieldTypeIcon <FieldTypeIcon type={indexedFieldName} tooltipEnabled={false} needsAria={true} />
type={indexedFieldName}
fieldName={item.name}
tooltipEnabled={false}
needsAria={true}
/>
</EuiFlexItem> </EuiFlexItem>
)} )}
</EuiFlexGroup> </EuiFlexGroup>

View file

@ -0,0 +1,20 @@
.dvSearchPanel__controls {
flex-direction: row;
padding: $euiSizeS;
}
@include euiBreakpoint('xs', 's', 'm', 'l') {
.dvSearchPanel__container {
flex-direction: column;
}
.dvSearchBar {
min-width: #{'max(100%, 500px)'};
}
.dvSearchPanel__controls {
padding: 0;
}
// prevent margin -16 which scrunches the filter bar
.globalFilterGroup__wrapper-isVisible {
margin: 0 !important;
}
}

View file

@ -6,21 +6,22 @@
*/ */
import React, { FC, useEffect, useState } from 'react'; import React, { FC, useEffect, useState } from 'react';
import { EuiCode, EuiFlexItem, EuiFlexGroup, EuiInputPopover } from '@elastic/eui'; import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { Query, fromKueryExpression, luceneStringToDsl, toElasticsearchQuery } from '@kbn/es-query'; import { Query, Filter } from '@kbn/es-query';
import { QueryStringInput } from '../../../../../../../../src/plugins/data/public';
import { ShardSizeFilter } from './shard_size_select'; import { ShardSizeFilter } from './shard_size_select';
import { DataVisualizerFieldNamesFilter } from './field_name_filter'; import { DataVisualizerFieldNamesFilter } from './field_name_filter';
import { DatavisualizerFieldTypeFilter } from './field_type_filter'; import { DataVisualizerFieldTypeFilter } from './field_type_filter';
import { IndexPattern } from '../../../../../../../../src/plugins/data/common';
import { JobFieldType } from '../../../../../common/types';
import { import {
ErrorMessage, IndexPattern,
SEARCH_QUERY_LANGUAGE, IndexPatternField,
SearchQueryLanguage, TimeRange,
} from '../../types/combined_query'; } from '../../../../../../../../src/plugins/data/common';
import { JobFieldType } from '../../../../../common/types';
import { SearchQueryLanguage } from '../../types/combined_query';
import { useDataVisualizerKibana } from '../../../kibana_context';
import './_index.scss';
import { createMergedEsQuery } from '../../utils/saved_search_utils';
interface Props { interface Props {
indexPattern: IndexPattern; indexPattern: IndexPattern;
searchString: Query['query']; searchString: Query['query'];
@ -38,12 +39,15 @@ interface Props {
searchQuery, searchQuery,
searchString, searchString,
queryLanguage, queryLanguage,
filters,
}: { }: {
searchQuery: Query['query']; searchQuery: Query['query'];
searchString: Query['query']; searchString: Query['query'];
queryLanguage: SearchQueryLanguage; queryLanguage: SearchQueryLanguage;
filters: Filter[];
}): void; }): void;
showEmptyFields: boolean; showEmptyFields: boolean;
onAddFilter?: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
} }
export const SearchPanel: FC<Props> = ({ export const SearchPanel: FC<Props> = ({
@ -61,98 +65,109 @@ export const SearchPanel: FC<Props> = ({
setSearchParams, setSearchParams,
showEmptyFields, showEmptyFields,
}) => { }) => {
const {
services: {
uiSettings,
notifications: { toasts },
data: {
query: queryManager,
ui: { SearchBar },
},
},
} = useDataVisualizerKibana();
// The internal state of the input query bar updated on every key stroke. // The internal state of the input query bar updated on every key stroke.
const [searchInput, setSearchInput] = useState<Query>({ const [searchInput, setSearchInput] = useState<Query>({
query: searchString || '', query: searchString || '',
language: searchQueryLanguage, language: searchQueryLanguage,
}); });
const [errorMessage, setErrorMessage] = useState<ErrorMessage | undefined>(undefined);
useEffect(() => { useEffect(() => {
setSearchInput({ setSearchInput({
query: searchString || '', query: searchString || '',
language: searchQueryLanguage, language: searchQueryLanguage,
}); });
}, [searchQueryLanguage, searchString]); }, [searchQueryLanguage, searchString, queryManager.filterManager]);
const searchHandler = (query: Query) => { const searchHandler = ({ query, filters }: { query?: Query; filters?: Filter[] }) => {
let filterQuery; const mergedQuery = query ?? searchInput;
const mergedFilters = filters ?? queryManager.filterManager.getFilters();
try { try {
if (query.language === SEARCH_QUERY_LANGUAGE.KUERY) { if (mergedFilters) {
filterQuery = toElasticsearchQuery(fromKueryExpression(query.query), indexPattern); queryManager.filterManager.setFilters(mergedFilters);
} else if (query.language === SEARCH_QUERY_LANGUAGE.LUCENE) {
filterQuery = luceneStringToDsl(query.query);
} else {
filterQuery = {};
} }
const combinedQuery = createMergedEsQuery(
mergedQuery,
queryManager.filterManager.getFilters() ?? [],
indexPattern,
uiSettings
);
setSearchParams({ setSearchParams({
searchQuery: filterQuery, searchQuery: combinedQuery,
searchString: query.query, searchString: mergedQuery.query,
queryLanguage: query.language as SearchQueryLanguage, queryLanguage: mergedQuery.language as SearchQueryLanguage,
filters: mergedFilters,
}); });
} catch (e) { } catch (e) {
console.log('Invalid syntax', JSON.stringify(e, null, 2)); // eslint-disable-line no-console console.log('Invalid syntax', JSON.stringify(e, null, 2)); // eslint-disable-line no-console
setErrorMessage({ query: query.query as string, message: e.message }); toasts.addError(e, {
title: i18n.translate('xpack.dataVisualizer.searchPanel.invalidSyntax', {
defaultMessage: 'Invalid syntax',
}),
});
} }
}; };
const searchChangeHandler = (query: Query) => setSearchInput(query);
return ( return (
<EuiFlexGroup gutterSize="m" alignItems="center" data-test-subj="dataVisualizerSearchPanel"> <EuiFlexGroup
<EuiFlexItem> gutterSize="s"
<EuiInputPopover alignItems="flexStart"
style={{ maxWidth: '100%' }} data-test-subj="dataVisualizerSearchPanel"
closePopover={() => setErrorMessage(undefined)} className={'dvSearchPanel__container'}
input={ responsive={false}
<QueryStringInput >
bubbleSubmitEvent={false} <EuiFlexItem grow={9} className={'dvSearchBar'}>
query={searchInput} <SearchBar
indexPatterns={[indexPattern]} dataTestSubj="dataVisualizerQueryInput"
onChange={searchChangeHandler} appName={'dataVisualizer'}
onSubmit={searchHandler} showFilterBar={true}
placeholder={i18n.translate( showDatePicker={false}
'xpack.dataVisualizer.searchPanel.queryBarPlaceholderText', showQueryInput={true}
{ query={searchInput}
defaultMessage: 'Search… (e.g. status:200 AND extension:"PHP")', onQuerySubmit={(params: { dateRange: TimeRange; query?: Query | undefined }) =>
} searchHandler({ query: params.query })
)}
disableAutoFocus={true}
dataTestSubj="dataVisualizerQueryInput"
languageSwitcherPopoverAnchorPosition="rightDown"
/>
} }
isOpen={errorMessage?.query === searchInput.query && errorMessage?.message !== ''} // @ts-expect-error onFiltersUpdated is a valid prop on SearchBar
> onFiltersUpdated={(filters: Filter[]) => searchHandler({ filters })}
<EuiCode> indexPatterns={[indexPattern]}
{i18n.translate( placeholder={i18n.translate('xpack.dataVisualizer.searchPanel.queryBarPlaceholderText', {
'xpack.dataVisualizer.searchPanel.invalidKuerySyntaxErrorMessageQueryBar', defaultMessage: 'Search… (e.g. status:200 AND extension:"PHP")',
{ })}
defaultMessage: 'Invalid query', displayStyle={'inPage'}
} isClearable={true}
)} customSubmitButton={<div />}
{': '} />
{errorMessage?.message.split('\n')[0]}
</EuiCode>
</EuiInputPopover>
</EuiFlexItem> </EuiFlexItem>
<EuiFlexItem grow={false}> <EuiFlexItem grow={2} className={'dvSearchPanel__controls'}>
<ShardSizeFilter <ShardSizeFilter
samplerShardSize={samplerShardSize} samplerShardSize={samplerShardSize}
setSamplerShardSize={setSamplerShardSize} setSamplerShardSize={setSamplerShardSize}
/> />
<DataVisualizerFieldNamesFilter
overallStats={overallStats}
setVisibleFieldNames={setVisibleFieldNames}
visibleFieldNames={visibleFieldNames}
showEmptyFields={showEmptyFields}
/>
<DataVisualizerFieldTypeFilter
indexedFieldTypes={indexedFieldTypes}
setVisibleFieldTypes={setVisibleFieldTypes}
visibleFieldTypes={visibleFieldTypes}
/>
</EuiFlexItem> </EuiFlexItem>
<DataVisualizerFieldNamesFilter
overallStats={overallStats}
setVisibleFieldNames={setVisibleFieldNames}
visibleFieldNames={visibleFieldNames}
showEmptyFields={showEmptyFields}
/>
<DatavisualizerFieldTypeFilter
indexedFieldTypes={indexedFieldTypes}
setVisibleFieldTypes={setVisibleFieldTypes}
visibleFieldTypes={visibleFieldTypes}
/>
</EuiFlexGroup> </EuiFlexGroup>
); );
}; };

View file

@ -49,6 +49,7 @@ export const DataVisualizerUrlStateContextProvider: FC<DataVisualizerUrlStateCon
}, },
} = useDataVisualizerKibana(); } = useDataVisualizerKibana();
const history = useHistory(); const history = useHistory();
const { search: searchString } = useLocation();
const [currentIndexPattern, setCurrentIndexPattern] = useState<IndexPattern | undefined>( const [currentIndexPattern, setCurrentIndexPattern] = useState<IndexPattern | undefined>(
undefined undefined
@ -56,7 +57,6 @@ export const DataVisualizerUrlStateContextProvider: FC<DataVisualizerUrlStateCon
const [currentSavedSearch, setCurrentSavedSearch] = useState<SimpleSavedObject<unknown> | null>( const [currentSavedSearch, setCurrentSavedSearch] = useState<SimpleSavedObject<unknown> | null>(
null null
); );
const { search: searchString } = useLocation();
useEffect(() => { useEffect(() => {
const prevSearchString = searchString; const prevSearchString = searchString;

View file

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

View file

@ -0,0 +1,105 @@
/*
* 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 { IndexDataVisualizerLocatorDefinition } from './locator';
describe('Index data visualizer locator', () => {
const definition = new IndexDataVisualizerLocatorDefinition();
it('should generate valid URL for the Index Data Visualizer Viewer page with global settings', async () => {
const location = await definition.getLocation({
indexPatternId: '3da93760-e0af-11ea-9ad3-3bcfc330e42a',
timeRange: {
from: 'now-30m',
to: 'now',
},
refreshInterval: { pause: false, value: 300 },
});
expect(location).toMatchObject({
app: 'ml',
path: '/jobs/new_job/datavisualizer?index=3da93760-e0af-11ea-9ad3-3bcfc330e42a&_a=(DATA_VISUALIZER_INDEX_VIEWER:())&_g=(refreshInterval:(pause:!f,value:300),time:(from:now-30m,to:now))',
state: {},
});
});
it('should prioritize savedSearchId even when index pattern id is available', async () => {
const location = await definition.getLocation({
indexPatternId: '3da93760-e0af-11ea-9ad3-3bcfc330e42a',
savedSearchId: '45014020-dffa-11eb-b120-a105fbbe93b3',
});
expect(location).toMatchObject({
app: 'ml',
path: '/jobs/new_job/datavisualizer?savedSearchId=45014020-dffa-11eb-b120-a105fbbe93b3&_a=(DATA_VISUALIZER_INDEX_VIEWER:())&_g=()',
state: {},
});
});
it('should generate valid URL with field names and field types', async () => {
const location = await definition.getLocation({
indexPatternId: '3da93760-e0af-11ea-9ad3-3bcfc330e42a',
visibleFieldNames: ['@timestamp', 'responsetime'],
visibleFieldTypes: ['number'],
});
expect(location).toMatchObject({
app: 'ml',
path: "/jobs/new_job/datavisualizer?index=3da93760-e0af-11ea-9ad3-3bcfc330e42a&_a=(DATA_VISUALIZER_INDEX_VIEWER:(visibleFieldNames:!('@timestamp',responsetime),visibleFieldTypes:!(number)))&_g=()",
});
});
it('should generate valid URL with KQL query', async () => {
const location = await definition.getLocation({
indexPatternId: '3da93760-e0af-11ea-9ad3-3bcfc330e42a',
query: {
searchQuery: {
bool: {
should: [
{
match: {
region: 'ap-northwest-1',
},
},
],
minimum_should_match: 1,
},
},
searchString: 'region : ap-northwest-1',
searchQueryLanguage: 'kuery',
},
});
expect(location).toMatchObject({
app: 'ml',
path: "/jobs/new_job/datavisualizer?index=3da93760-e0af-11ea-9ad3-3bcfc330e42a&_a=(DATA_VISUALIZER_INDEX_VIEWER:(searchQuery:(bool:(minimum_should_match:1,should:!((match:(region:ap-northwest-1))))),searchQueryLanguage:kuery,searchString:'region : ap-northwest-1'))&_g=()",
state: {},
});
});
it('should generate valid URL with Lucene query', async () => {
const location = await definition.getLocation({
indexPatternId: '3da93760-e0af-11ea-9ad3-3bcfc330e42a',
query: {
searchQuery: {
query_string: {
query: 'region: ap-northwest-1',
analyze_wildcard: true,
},
},
searchString: 'region : ap-northwest-1',
searchQueryLanguage: 'lucene',
},
});
expect(location).toMatchObject({
app: 'ml',
path: "/jobs/new_job/datavisualizer?index=3da93760-e0af-11ea-9ad3-3bcfc330e42a&_a=(DATA_VISUALIZER_INDEX_VIEWER:(searchQuery:(query_string:(analyze_wildcard:!t,query:'region: ap-northwest-1')),searchQueryLanguage:lucene,searchString:'region : ap-northwest-1'))&_g=()",
state: {},
});
});
});

View file

@ -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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
// @ts-ignore
import { encode } from 'rison-node';
import { stringify } from 'query-string';
import { SerializableRecord } from '@kbn/utility-types';
import { RefreshInterval, TimeRange } from '../../../../../../../src/plugins/data/common';
import { LocatorDefinition, LocatorPublic } from '../../../../../../../src/plugins/share/common';
import { QueryState } from '../../../../../../../src/plugins/data/public';
import { Dictionary, isRisonSerializationRequired } from '../../common/util/url_state';
import { SearchQueryLanguage } from '../types/combined_query';
export const DATA_VISUALIZER_APP_LOCATOR = 'DATA_VISUALIZER_APP_LOCATOR';
export interface IndexDataVisualizerLocatorParams extends SerializableRecord {
/**
* Optionally set saved search ID.
*/
savedSearchId?: string;
/**
* Optionally set index pattern ID.
*/
indexPatternId?: string;
/**
* Optionally set the time range in the time picker.
*/
timeRange?: TimeRange;
/**
* Optionally set the refresh interval.
*/
refreshInterval?: RefreshInterval & SerializableRecord;
/**
* Optionally set a query.
*/
query?: {
searchQuery: SerializableRecord;
searchString: string | SerializableRecord;
searchQueryLanguage: SearchQueryLanguage;
};
/**
* If not given, will use the uiSettings configuration for `storeInSessionStorage`. useHash determines
* whether to hash the data in the url to avoid url length issues.
*/
useHash?: boolean;
/**
* Optionally set visible field names.
*/
visibleFieldNames?: string[];
/**
* Optionally set visible field types.
*/
visibleFieldTypes?: string[];
}
export type IndexDataVisualizerLocator = LocatorPublic<IndexDataVisualizerLocatorParams>;
export class IndexDataVisualizerLocatorDefinition
implements LocatorDefinition<IndexDataVisualizerLocatorParams>
{
public readonly id = DATA_VISUALIZER_APP_LOCATOR;
constructor() {}
public readonly getLocation = async (params: IndexDataVisualizerLocatorParams) => {
const {
indexPatternId,
query,
refreshInterval,
savedSearchId,
timeRange,
visibleFieldNames,
visibleFieldTypes,
} = params;
const appState: {
searchQuery?: { [key: string]: any };
searchQueryLanguage?: string;
searchString?: string | SerializableRecord;
visibleFieldNames?: string[];
visibleFieldTypes?: string[];
} = {};
const queryState: QueryState = {};
if (query) {
appState.searchQuery = query.searchQuery;
appState.searchString = query.searchString;
appState.searchQueryLanguage = query.searchQueryLanguage;
}
if (visibleFieldNames) appState.visibleFieldNames = visibleFieldNames;
if (visibleFieldTypes) appState.visibleFieldTypes = visibleFieldTypes;
if (timeRange) queryState.time = timeRange;
if (refreshInterval) queryState.refreshInterval = refreshInterval;
const urlState: Dictionary<any> = {
...(savedSearchId ? { savedSearchId } : { index: indexPatternId }),
_a: { DATA_VISUALIZER_INDEX_VIEWER: appState },
_g: queryState,
};
const parsedQueryString: Dictionary<any> = {};
Object.keys(urlState).forEach((a) => {
if (isRisonSerializationRequired(a)) {
parsedQueryString[a] = encode(urlState[a]);
} else {
parsedQueryString[a] = urlState[a];
}
});
const newLocationSearchString = stringify(parsedQueryString, {
sort: false,
encode: false,
});
const path = `/jobs/new_job/datavisualizer?${newLocationSearchString}`;
return {
app: 'ml',
path,
state: {},
};
};
}

View file

@ -5,6 +5,7 @@
* 2.0. * 2.0.
*/ */
import type { Filter } from '@kbn/es-query';
import { Query } from '../../../../../../../src/plugins/data/common/query'; import { Query } from '../../../../../../../src/plugins/data/common/query';
import { SearchQueryLanguage } from './combined_query'; import { SearchQueryLanguage } from './combined_query';
@ -25,4 +26,5 @@ export interface DataVisualizerIndexBasedAppState extends Omit<ListingPageUrlSta
showDistributions?: boolean; showDistributions?: boolean;
showAllFields?: boolean; showAllFields?: boolean;
showEmptyFields?: boolean; showEmptyFields?: boolean;
filters?: Filter[];
} }

View file

@ -0,0 +1,313 @@
/*
* 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 {
getQueryFromSavedSearch,
createMergedEsQuery,
getEsQueryFromSavedSearch,
} from './saved_search_utils';
import type { SavedSearchSavedObject } from '../../../../common';
import type { SavedSearch } from '../../../../../../../src/plugins/discover/public';
import type { Filter, FilterStateStore } from '@kbn/es-query';
import { stubbedSavedObjectIndexPattern } from '../../../../../../../src/plugins/data/common/data_views/data_view.stub';
import { IndexPattern } from '../../../../../../../src/plugins/data/common';
import { fieldFormatsMock } from '../../../../../../../src/plugins/field_formats/common/mocks';
import { uiSettingsServiceMock } from 'src/core/public/mocks';
// helper function to create index patterns
function createMockDataView(id: string) {
const {
type,
version,
attributes: { timeFieldName, fields, title },
} = stubbedSavedObjectIndexPattern(id);
return new IndexPattern({
spec: {
id,
type,
version,
timeFieldName,
fields: JSON.parse(fields),
title,
runtimeFieldMap: {},
},
fieldFormats: fieldFormatsMock,
shortDotsEnable: false,
metaFields: [],
});
}
const mockDataView = createMockDataView('test-mock-data-view');
const mockUiSettings = uiSettingsServiceMock.createStartContract();
// @ts-expect-error We don't need the full object here
const luceneSavedSearchObj: SavedSearchSavedObject = {
attributes: {
title: 'farequote_filter_and_lucene',
columns: ['_source'],
sort: ['@timestamp', 'desc'],
kibanaSavedObjectMeta: {
searchSourceJSON:
'{"highlightAll":true,"version":true,"query":{"query":"responsetime:>50","language":"lucene"},"filter":[{"meta":{"index":"90a978e0-1c80-11ec-b1d7-f7e5cf21b9e0","negate":false,"disabled":false,"alias":null,"type":"phrase","key":"airline","value":"ASA","params":{"query":"ASA","type":"phrase"}},"query":{"match":{"airline":{"query":"ASA","type":"phrase"}}},"$state":{"store":"appState"}}],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}',
},
},
id: '93fc4d60-1c80-11ec-b1d7-f7e5cf21b9e0',
type: 'search',
};
// @ts-expect-error We don't need the full object here
const luceneInvalidSavedSearchObj: SavedSearchSavedObject = {
attributes: {
kibanaSavedObjectMeta: {
searchSourceJSON: null,
},
},
id: '93fc4d60-1c80-11ec-b1d7-f7e5cf21b9e0',
type: 'search',
};
const kqlSavedSearch: SavedSearch = {
title: 'farequote_filter_and_kuery',
description: '',
columns: ['_source'],
// @ts-expect-error We don't need the full object here
kibanaSavedObjectMeta: {
searchSourceJSON:
'{"highlightAll":true,"version":true,"query":{"query":"responsetime > 49","language":"kuery"},"filter":[{"meta":{"index":"90a978e0-1c80-11ec-b1d7-f7e5cf21b9e0","negate":false,"disabled":false,"alias":null,"type":"phrase","key":"airline","value":"ASA","params":{"query":"ASA","type":"phrase"}},"query":{"match":{"airline":{"query":"ASA","type":"phrase"}}},"$state":{"store":"appState"}}],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}',
},
};
describe('getQueryFromSavedSearch()', () => {
it('should return parsed searchSourceJSON with query and filter', () => {
expect(getQueryFromSavedSearch(luceneSavedSearchObj)).toEqual({
filter: [
{
$state: { store: 'appState' },
meta: {
alias: null,
disabled: false,
index: '90a978e0-1c80-11ec-b1d7-f7e5cf21b9e0',
key: 'airline',
negate: false,
params: { query: 'ASA', type: 'phrase' },
type: 'phrase',
value: 'ASA',
},
query: { match: { airline: { query: 'ASA', type: 'phrase' } } },
},
],
highlightAll: true,
indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index',
query: { language: 'lucene', query: 'responsetime:>50' },
version: true,
});
expect(getQueryFromSavedSearch(kqlSavedSearch)).toEqual({
filter: [
{
$state: { store: 'appState' },
meta: {
alias: null,
disabled: false,
index: '90a978e0-1c80-11ec-b1d7-f7e5cf21b9e0',
key: 'airline',
negate: false,
params: { query: 'ASA', type: 'phrase' },
type: 'phrase',
value: 'ASA',
},
query: { match: { airline: { query: 'ASA', type: 'phrase' } } },
},
],
highlightAll: true,
indexRefName: 'kibanaSavedObjectMeta.searchSourceJSON.index',
query: { language: 'kuery', query: 'responsetime > 49' },
version: true,
});
});
it('should return undefined if invalid searchSourceJSON', () => {
expect(getQueryFromSavedSearch(luceneInvalidSavedSearchObj)).toEqual(undefined);
});
});
describe('createMergedEsQuery()', () => {
const luceneQuery = {
query: 'responsetime:>50',
language: 'lucene',
};
const kqlQuery = {
query: 'responsetime > 49',
language: 'kuery',
};
const mockFilters: Filter[] = [
{
meta: {
index: '90a978e0-1c80-11ec-b1d7-f7e5cf21b9e0',
negate: false,
disabled: false,
alias: null,
type: 'phrase',
key: 'airline',
params: {
query: 'ASA',
},
},
query: {
match: {
airline: {
query: 'ASA',
type: 'phrase',
},
},
},
$state: {
store: 'appState' as FilterStateStore,
},
},
];
it('return formatted ES bool query with both the original query and filters combined', () => {
expect(createMergedEsQuery(luceneQuery, mockFilters)).toEqual({
bool: {
filter: [{ match_phrase: { airline: { query: 'ASA' } } }],
must: [{ query_string: { query: 'responsetime:>50' } }],
must_not: [],
should: [],
},
});
expect(createMergedEsQuery(kqlQuery, mockFilters)).toEqual({
bool: {
filter: [{ match_phrase: { airline: { query: 'ASA' } } }],
minimum_should_match: 1,
must_not: [],
should: [{ range: { responsetime: { gt: '49' } } }],
},
});
});
it('return formatted ES bool query without filters ', () => {
expect(createMergedEsQuery(luceneQuery)).toEqual({
bool: {
filter: [],
must: [{ query_string: { query: 'responsetime:>50' } }],
must_not: [],
should: [],
},
});
expect(createMergedEsQuery(kqlQuery)).toEqual({
bool: {
filter: [],
minimum_should_match: 1,
must_not: [],
should: [{ range: { responsetime: { gt: '49' } } }],
},
});
});
});
describe('getEsQueryFromSavedSearch()', () => {
it('return undefined if saved search is not provided', () => {
expect(
getEsQueryFromSavedSearch({
indexPattern: mockDataView,
savedSearch: undefined,
uiSettings: mockUiSettings,
})
).toEqual(undefined);
});
it('return search data from saved search if neither query nor filter is provided ', () => {
expect(
getEsQueryFromSavedSearch({
indexPattern: mockDataView,
savedSearch: luceneSavedSearchObj,
uiSettings: mockUiSettings,
})
).toEqual({
queryLanguage: 'lucene',
searchQuery: {
bool: {
filter: [{ match_phrase: { airline: { query: 'ASA' } } }],
must: [{ query_string: { query: 'responsetime:>50' } }],
must_not: [],
should: [],
},
},
searchString: 'responsetime:>50',
});
});
it('should override original saved search with the provided query ', () => {
expect(
getEsQueryFromSavedSearch({
indexPattern: mockDataView,
savedSearch: luceneSavedSearchObj,
uiSettings: mockUiSettings,
query: {
query: 'responsetime:>100',
language: 'lucene',
},
})
).toEqual({
queryLanguage: 'lucene',
searchQuery: {
bool: {
filter: [{ match_phrase: { airline: { query: 'ASA' } } }],
must: [{ query_string: { query: 'responsetime:>100' } }],
must_not: [],
should: [],
},
},
searchString: 'responsetime:>100',
});
});
it('should override original saved search with the provided filters ', () => {
expect(
getEsQueryFromSavedSearch({
indexPattern: mockDataView,
savedSearch: luceneSavedSearchObj,
uiSettings: mockUiSettings,
query: {
query: 'responsetime:>100',
language: 'lucene',
},
filters: [
{
meta: {
index: '90a978e0-1c80-11ec-b1d7-f7e5cf21b9e0',
alias: null,
negate: true,
disabled: false,
type: 'phrase',
key: 'airline',
params: {
query: 'JZA',
},
},
query: {
match_phrase: {
airline: 'JZA',
},
},
$state: {
store: 'appState' as FilterStateStore,
},
},
],
})
).toEqual({
queryLanguage: 'lucene',
searchQuery: {
bool: {
filter: [],
must: [{ query_string: { query: 'responsetime:>100' } }],
must_not: [{ match_phrase: { airline: 'JZA' } }],
should: [],
},
},
searchString: 'responsetime:>100',
});
});
});

View file

@ -8,55 +8,155 @@
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { IUiSettingsClient } from 'kibana/public'; import { IUiSettingsClient } from 'kibana/public';
import { import {
buildEsQuery,
buildQueryFromFilters,
decorateQuery,
fromKueryExpression, fromKueryExpression,
luceneStringToDsl,
toElasticsearchQuery, toElasticsearchQuery,
buildQueryFromFilters,
buildEsQuery,
Query,
Filter,
} from '@kbn/es-query'; } from '@kbn/es-query';
import { estypes } from '@elastic/elasticsearch'; import { isSavedSearchSavedObject, SavedSearchSavedObject } from '../../../../common/types';
import { SavedSearchSavedObject } from '../../../../common/types';
import { IndexPattern } from '../../../../../../../src/plugins/data/common'; import { IndexPattern } from '../../../../../../../src/plugins/data/common';
import { SEARCH_QUERY_LANGUAGE, SearchQueryLanguage } from '../types/combined_query'; import { SEARCH_QUERY_LANGUAGE, SearchQueryLanguage } from '../types/combined_query';
import { getEsQueryConfig, Query } from '../../../../../../../src/plugins/data/public'; import { SavedSearch } from '../../../../../../../src/plugins/discover/public';
import { getEsQueryConfig } from '../../../../../../../src/plugins/data/common';
import { FilterManager } from '../../../../../../../src/plugins/data/public';
export function getQueryFromSavedSearch(savedSearch: SavedSearchSavedObject) { /**
const search = savedSearch.attributes.kibanaSavedObjectMeta as { searchSourceJSON: string }; * Parse the stringified searchSourceJSON
return JSON.parse(search.searchSourceJSON) as { * from a saved search or saved search object
query: Query; */
filter: any[]; export function getQueryFromSavedSearch(savedSearch: SavedSearchSavedObject | SavedSearch) {
}; const search = isSavedSearchSavedObject(savedSearch)
? savedSearch?.attributes?.kibanaSavedObjectMeta
: // @ts-expect-error kibanaSavedObjectMeta does exist
savedSearch?.kibanaSavedObjectMeta;
const parsed =
typeof search?.searchSourceJSON === 'string'
? (JSON.parse(search.searchSourceJSON) as {
query: Query;
filter: Filter[];
})
: undefined;
// Remove indexRefName because saved search might no longer be relevant
// if user modifies the query or filter
// after opening a saved search
if (parsed && Array.isArray(parsed.filter)) {
parsed.filter.forEach((f) => {
// @ts-expect-error indexRefName does appear in meta for newly created saved search
f.meta.indexRefName = undefined;
});
}
return parsed;
} }
/** /**
* Extract query data from the saved search object. * Create an Elasticsearch query that combines both lucene/kql query string and filters
* Should also form a valid query if only the query or filters is provided
*/ */
export function extractSearchData( export function createMergedEsQuery(
savedSearch: SavedSearchSavedObject | null, query?: Query,
currentIndexPattern: IndexPattern, filters?: Filter[],
queryStringOptions: Record<string, any> | string indexPattern?: IndexPattern,
uiSettings?: IUiSettingsClient
) { ) {
if (!savedSearch) { let combinedQuery: any = getDefaultQuery();
return undefined;
if (query && query.language === SEARCH_QUERY_LANGUAGE.KUERY) {
const ast = fromKueryExpression(query.query);
if (query.query !== '') {
combinedQuery = toElasticsearchQuery(ast, indexPattern);
}
const filterQuery = buildQueryFromFilters(filters, indexPattern);
if (Array.isArray(combinedQuery.bool.filter) === false) {
combinedQuery.bool.filter =
combinedQuery.bool.filter === undefined ? [] : [combinedQuery.bool.filter];
}
if (Array.isArray(combinedQuery.bool.must_not) === false) {
combinedQuery.bool.must_not =
combinedQuery.bool.must_not === undefined ? [] : [combinedQuery.bool.must_not];
}
combinedQuery.bool.filter = [...combinedQuery.bool.filter, ...filterQuery.filter];
combinedQuery.bool.must_not = [...combinedQuery.bool.must_not, ...filterQuery.must_not];
} else {
combinedQuery = buildEsQuery(
indexPattern,
query ? [query] : [],
filters ? filters : [],
uiSettings ? getEsQueryConfig(uiSettings) : undefined
);
}
return combinedQuery;
}
/**
* Extract query data from the saved search object
* with overrides from the provided query data and/or filters
*/
export function getEsQueryFromSavedSearch({
indexPattern,
uiSettings,
savedSearch,
query,
filters,
filterManager,
}: {
indexPattern: IndexPattern;
uiSettings: IUiSettingsClient;
savedSearch: SavedSearchSavedObject | SavedSearch | null | undefined;
query?: Query;
filters?: Filter[];
filterManager?: FilterManager;
}) {
if (!indexPattern || !savedSearch) return;
const savedSearchData = getQueryFromSavedSearch(savedSearch);
const userQuery = query;
const userFilters = filters;
// If no saved search available, use user's query and filters
if (!savedSearchData && userQuery) {
if (filterManager && userFilters) filterManager.setFilters(userFilters);
const combinedQuery = createMergedEsQuery(
userQuery,
Array.isArray(userFilters) ? userFilters : [],
indexPattern,
uiSettings
);
return {
searchQuery: combinedQuery,
searchString: userQuery.query,
queryLanguage: userQuery.language as SearchQueryLanguage,
};
} }
const { query: extractedQuery } = getQueryFromSavedSearch(savedSearch); // If saved search available, merge saved search with latest user query or filters differ from extracted saved search data
const queryLanguage = extractedQuery.language as SearchQueryLanguage; if (savedSearchData) {
const qryString = extractedQuery.query; const currentQuery = userQuery ?? savedSearchData?.query;
let qry; const currentFilters = userFilters ?? savedSearchData?.filter;
if (queryLanguage === SEARCH_QUERY_LANGUAGE.KUERY) {
const ast = fromKueryExpression(qryString); if (filterManager) filterManager.setFilters(currentFilters);
qry = toElasticsearchQuery(ast, currentIndexPattern);
} else { const combinedQuery = createMergedEsQuery(
qry = luceneStringToDsl(qryString); currentQuery,
decorateQuery(qry, queryStringOptions); Array.isArray(currentFilters) ? currentFilters : [],
indexPattern,
uiSettings
);
return {
searchQuery: combinedQuery,
searchString: currentQuery.query,
queryLanguage: currentQuery.language as SearchQueryLanguage,
};
} }
return {
searchQuery: qry,
searchString: qryString,
queryLanguage,
};
} }
const DEFAULT_QUERY = { const DEFAULT_QUERY = {
@ -69,64 +169,6 @@ const DEFAULT_QUERY = {
}, },
}; };
export function getDefaultDatafeedQuery() { export function getDefaultQuery() {
return cloneDeep(DEFAULT_QUERY); return cloneDeep(DEFAULT_QUERY);
} }
export function createSearchItems(
kibanaConfig: IUiSettingsClient,
indexPattern: IndexPattern | undefined,
savedSearch: SavedSearchSavedObject | null
) {
// query is only used by the data visualizer as it needs
// a lucene query_string.
// Using a blank query will cause match_all:{} to be used
// when passed through luceneStringToDsl
let query: Query = {
query: '',
language: 'lucene',
};
let combinedQuery: estypes.QueryDslQueryContainer = getDefaultDatafeedQuery();
if (savedSearch !== null) {
const data = getQueryFromSavedSearch(savedSearch);
query = data.query;
const filter = data.filter;
const filters = Array.isArray(filter) ? filter : [];
if (query.language === SEARCH_QUERY_LANGUAGE.KUERY) {
const ast = fromKueryExpression(query.query);
if (query.query !== '') {
combinedQuery = toElasticsearchQuery(ast, indexPattern);
}
const filterQuery = buildQueryFromFilters(filters, indexPattern);
if (!combinedQuery.bool) {
throw new Error('Missing bool on query');
}
if (!Array.isArray(combinedQuery.bool.filter)) {
combinedQuery.bool.filter =
combinedQuery.bool.filter === undefined ? [] : [combinedQuery.bool.filter];
}
if (!Array.isArray(combinedQuery.bool.must_not)) {
combinedQuery.bool.must_not =
combinedQuery.bool.must_not === undefined ? [] : [combinedQuery.bool.must_not];
}
combinedQuery.bool.filter = [...combinedQuery.bool.filter, ...filterQuery.filter];
combinedQuery.bool.must_not = [...combinedQuery.bool.must_not, ...filterQuery.must_not];
} else {
const esQueryConfigs = getEsQueryConfig(kibanaConfig);
combinedQuery = buildEsQuery(indexPattern, [query], filters, esQueryConfigs);
}
}
return {
query,
combinedQuery,
};
}

View file

@ -48,7 +48,10 @@ export class DataVisualizerPlugin
DataVisualizerStartDependencies DataVisualizerStartDependencies
> >
{ {
public setup(core: CoreSetup, plugins: DataVisualizerSetupDependencies) { public setup(
core: CoreSetup<DataVisualizerStartDependencies, DataVisualizerPluginStart>,
plugins: DataVisualizerSetupDependencies
) {
if (plugins.home) { if (plugins.home) {
registerHomeAddData(plugins.home); registerHomeAddData(plugins.home);
registerHomeFeatureCatalogue(plugins.home); registerHomeFeatureCatalogue(plugins.home);

View file

@ -83,6 +83,7 @@ const PageWrapper: FC<IndexOrSearchPageProps> = ({ nextStepPath, deps, mode }) =
application: { navigateToUrl }, application: { navigateToUrl },
}, },
} = useMlKibana(); } = useMlKibana();
const { redirectToMlAccessDeniedPage } = deps; const { redirectToMlAccessDeniedPage } = deps;
const redirectToJobsManagementPage = useCreateAndNavigateToMlLink( const redirectToJobsManagementPage = useCreateAndNavigateToMlLink(
ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE

View file

@ -9,7 +9,7 @@ import { MlLocatorDefinition } from './ml_locator';
import { ML_PAGES } from '../../common/constants/locator'; import { ML_PAGES } from '../../common/constants/locator';
import { ANALYSIS_CONFIG_TYPE } from '../../common/constants/data_frame_analytics'; import { ANALYSIS_CONFIG_TYPE } from '../../common/constants/data_frame_analytics';
describe('MlUrlGenerator', () => { describe('ML locator', () => {
const definition = new MlLocatorDefinition(); const definition = new MlLocatorDefinition();
describe('AnomalyDetection', () => { describe('AnomalyDetection', () => {

View file

@ -9044,7 +9044,6 @@
"xpack.dataVisualizer.removeCombinedFieldsLabel": "結合されたフィールドを削除", "xpack.dataVisualizer.removeCombinedFieldsLabel": "結合されたフィールドを削除",
"xpack.dataVisualizer.searchPanel.allFieldsLabel": "すべてのフィールド", "xpack.dataVisualizer.searchPanel.allFieldsLabel": "すべてのフィールド",
"xpack.dataVisualizer.searchPanel.allOptionLabel": "すべて検索", "xpack.dataVisualizer.searchPanel.allOptionLabel": "すべて検索",
"xpack.dataVisualizer.searchPanel.invalidKuerySyntaxErrorMessageQueryBar": "無効なクエリ",
"xpack.dataVisualizer.searchPanel.numberFieldsLabel": "数値フィールド", "xpack.dataVisualizer.searchPanel.numberFieldsLabel": "数値フィールド",
"xpack.dataVisualizer.searchPanel.ofFieldsTotal": "合計 {totalCount}", "xpack.dataVisualizer.searchPanel.ofFieldsTotal": "合計 {totalCount}",
"xpack.dataVisualizer.searchPanel.queryBarPlaceholder": "小さいサンプルサイズを選択することで、クエリの実行時間を短縮しクラスターへの負荷を軽減できます。", "xpack.dataVisualizer.searchPanel.queryBarPlaceholder": "小さいサンプルサイズを選択することで、クエリの実行時間を短縮しクラスターへの負荷を軽減できます。",

View file

@ -9131,7 +9131,6 @@
"xpack.dataVisualizer.removeCombinedFieldsLabel": "移除组合字段", "xpack.dataVisualizer.removeCombinedFieldsLabel": "移除组合字段",
"xpack.dataVisualizer.searchPanel.allFieldsLabel": "所有字段", "xpack.dataVisualizer.searchPanel.allFieldsLabel": "所有字段",
"xpack.dataVisualizer.searchPanel.allOptionLabel": "搜索全部", "xpack.dataVisualizer.searchPanel.allOptionLabel": "搜索全部",
"xpack.dataVisualizer.searchPanel.invalidKuerySyntaxErrorMessageQueryBar": "无效查询",
"xpack.dataVisualizer.searchPanel.numberFieldsLabel": "字段数目", "xpack.dataVisualizer.searchPanel.numberFieldsLabel": "字段数目",
"xpack.dataVisualizer.searchPanel.ofFieldsTotal": ",共 {totalCount} 个", "xpack.dataVisualizer.searchPanel.ofFieldsTotal": ",共 {totalCount} 个",
"xpack.dataVisualizer.searchPanel.queryBarPlaceholder": "选择较小的样例大小将减少查询运行时间和集群上的负载。", "xpack.dataVisualizer.searchPanel.queryBarPlaceholder": "选择较小的样例大小将减少查询运行时间和集群上的负载。",

View file

@ -110,11 +110,11 @@ export function MachineLearningDataVisualizerTableProvider(
if (!(await testSubjects.exists(this.detailsSelector(fieldName)))) { if (!(await testSubjects.exists(this.detailsSelector(fieldName)))) {
const selector = this.rowSelector( const selector = this.rowSelector(
fieldName, fieldName,
`dataVisualizerDetailsToggle-${fieldName}-arrowDown` `dataVisualizerDetailsToggle-${fieldName}-arrowRight`
); );
await testSubjects.click(selector); await testSubjects.click(selector);
await testSubjects.existOrFail( await testSubjects.existOrFail(
this.rowSelector(fieldName, `dataVisualizerDetailsToggle-${fieldName}-arrowUp`), this.rowSelector(fieldName, `dataVisualizerDetailsToggle-${fieldName}-arrowDown`),
{ {
timeout: 1000, timeout: 1000,
} }
@ -128,10 +128,10 @@ export function MachineLearningDataVisualizerTableProvider(
await retry.tryForTime(10000, async () => { await retry.tryForTime(10000, async () => {
if (await testSubjects.exists(this.detailsSelector(fieldName))) { if (await testSubjects.exists(this.detailsSelector(fieldName))) {
await testSubjects.click( await testSubjects.click(
this.rowSelector(fieldName, `dataVisualizerDetailsToggle-${fieldName}-arrowUp`) this.rowSelector(fieldName, `dataVisualizerDetailsToggle-${fieldName}-arrowDown`)
); );
await testSubjects.existOrFail( await testSubjects.existOrFail(
this.rowSelector(fieldName, `dataVisualizerDetailsToggle-${fieldName}-arrowDown`), this.rowSelector(fieldName, `dataVisualizerDetailsToggle-${fieldName}-arrowRight`),
{ {
timeout: 1000, timeout: 1000,
} }
@ -150,7 +150,7 @@ export function MachineLearningDataVisualizerTableProvider(
const docCount = await testSubjects.getVisibleText(docCountFormattedSelector); const docCount = await testSubjects.getVisibleText(docCountFormattedSelector);
expect(docCount).to.eql( expect(docCount).to.eql(
docCountFormatted, docCountFormatted,
`Expected field document count to be '${docCountFormatted}' (got '${docCount}')` `Expected field ${fieldName}'s document count to be '${docCountFormatted}' (got '${docCount}')`
); );
} }