mirror of
https://github.com/elastic/kibana.git
synced 2025-04-25 02:09:32 -04:00
[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:
parent
b73d939d6c
commit
747212ce45
75 changed files with 1749 additions and 824 deletions
|
@ -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>([
|
||||||
|
|
|
@ -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']);
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
.embeddedMapContent {
|
.embeddedMap__content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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={
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 },
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
.dvFieldTypeIcon__anchor {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
@import 'field_type_icon';
|
|
@ -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();
|
||||||
|
|
|
@ -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}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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: [],
|
||||||
|
|
|
@ -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`}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 */
|
||||||
|
|
|
@ -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', {
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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),
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'];
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
@import 'index_data_visualizer_view';
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
@import 'search_panel';
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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';
|
|
@ -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: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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: {},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
|
@ -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[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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": "小さいサンプルサイズを選択することで、クエリの実行時間を短縮しクラスターへの負荷を軽減できます。",
|
||||||
|
|
|
@ -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": "选择较小的样例大小将减少查询运行时间和集群上的负载。",
|
||||||
|
|
|
@ -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}')`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue