mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -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',
|
||||
} 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 NON_AGGREGATABLE_FIELD_TYPES = new Set<string>([
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import type { SimpleSavedObject } from 'kibana/public';
|
||||
import { isPopulatedObject } from '../utils/object_utils';
|
||||
export type { JobFieldType } from './job_field_type';
|
||||
export type {
|
||||
FieldRequestConfig,
|
||||
|
@ -27,3 +28,7 @@ export interface DataVisualizerTableState {
|
|||
}
|
||||
|
||||
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%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
|
|
|
@ -39,7 +39,7 @@ export function EmbeddedMapComponent({
|
|||
const baseLayers = useRef<LayerDescriptor[]>();
|
||||
|
||||
const {
|
||||
services: { embeddable: embeddablePlugin, maps: mapsPlugin },
|
||||
services: { embeddable: embeddablePlugin, maps: mapsPlugin, data },
|
||||
} = useDataVisualizerKibana();
|
||||
|
||||
const factory:
|
||||
|
@ -73,7 +73,7 @@ export function EmbeddedMapComponent({
|
|||
const input: MapEmbeddableInput = {
|
||||
id: htmlIdGenerator()(),
|
||||
attributes: { title: '' },
|
||||
filters: [],
|
||||
filters: data.query.filterManager.getFilters() ?? [],
|
||||
hidePanelTitles: true,
|
||||
viewMode: ViewMode.VIEW,
|
||||
isLayerTOCOpen: false,
|
||||
|
@ -143,7 +143,7 @@ export function EmbeddedMapComponent({
|
|||
return (
|
||||
<div
|
||||
data-test-subj="dataVisualizerEmbeddedMapContent"
|
||||
className="embeddedMapContent"
|
||||
className="embeddedMap__content"
|
||||
ref={embeddableRoot}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -11,6 +11,7 @@ import { EuiListGroup, EuiListGroupItem } from '@elastic/eui';
|
|||
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
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 {
|
||||
examples: Array<string | object>;
|
||||
}
|
||||
|
@ -31,8 +32,7 @@ export const ExamplesList: FC<Props> = ({ examples }) => {
|
|||
examplesContent = examples.map((example, i) => {
|
||||
return (
|
||||
<EuiListGroupItem
|
||||
className="fieldDataCard__codeContent"
|
||||
size="s"
|
||||
size="xs"
|
||||
key={`example_${i}`}
|
||||
label={typeof example === 'string' ? example : JSON.stringify(example)}
|
||||
/>
|
||||
|
@ -41,7 +41,10 @@ export const ExamplesList: FC<Props> = ({ examples }) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div data-test-subj="dataVisualizerFieldDataExamplesList">
|
||||
<ExpandedRowPanel
|
||||
dataTestSubj="dataVisualizerFieldDataExamplesList"
|
||||
className="dvText__wrapper dvPanel__wrapper"
|
||||
>
|
||||
<ExpandedRowFieldHeader>
|
||||
<FormattedMessage
|
||||
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}>
|
||||
{examplesContent}
|
||||
</EuiListGroup>
|
||||
</div>
|
||||
</ExpandedRowPanel>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -52,10 +52,7 @@ export const FileBasedDataVisualizerExpandedRow = ({ item }: { item: FileBasedFi
|
|||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="dataVisualizerFieldExpandedRow"
|
||||
data-test-subj={`dataVisualizerFieldExpandedRow-${fieldName}`}
|
||||
>
|
||||
<div className="dvExpandedRow" data-test-subj={`dataVisualizerFieldExpandedRow-${fieldName}`}>
|
||||
{getCardContent()}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -6,8 +6,6 @@
|
|||
*/
|
||||
|
||||
import React, { FC, useMemo } from 'react';
|
||||
|
||||
import { EuiFlexItem } from '@elastic/eui';
|
||||
import { Feature, Point } from 'geojson';
|
||||
import type { FieldDataRowProps } from '../../stats_table/types/field_data_row';
|
||||
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 { ExpandedRowContent } from '../../stats_table/components/field_data_expanded_row/expanded_row_content';
|
||||
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>.+)');
|
||||
|
||||
|
@ -63,17 +62,12 @@ export const GeoPointContent: FC<FieldDataRowProps> = ({ config }) => {
|
|||
<ExpandedRowContent dataTestSubj={'dataVisualizerGeoPointContent'}>
|
||||
<DocumentStatsTable config={config} />
|
||||
{formattedResults && Array.isArray(formattedResults.examples) && (
|
||||
<EuiFlexItem>
|
||||
<ExamplesList examples={formattedResults.examples} />
|
||||
</EuiFlexItem>
|
||||
<ExamplesList examples={formattedResults.examples} />
|
||||
)}
|
||||
{formattedResults && Array.isArray(formattedResults.layerList) && (
|
||||
<EuiFlexItem
|
||||
className={'dataVisualizerMapWrapper'}
|
||||
data-test-subj={'dataVisualizerEmbeddedMap'}
|
||||
>
|
||||
<ExpandedRowPanel className={'dvPanel__wrapper dvMap__wrapper'} grow={true}>
|
||||
<EmbeddedMapComponent layerList={formattedResults.layerList} />
|
||||
</EuiFlexItem>
|
||||
</ExpandedRowPanel>
|
||||
)}
|
||||
</ExpandedRowContent>
|
||||
);
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import React, { FC, useEffect, useState } from 'react';
|
||||
import { EuiFlexItem } from '@elastic/eui';
|
||||
import { IndexPattern } from '../../../../../../../../../src/plugins/data/common';
|
||||
import { CombinedQuery } from '../../../../index_data_visualizer/types/combined_query';
|
||||
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 { ES_GEO_FIELD_TYPE, LayerDescriptor } from '../../../../../../../maps/common';
|
||||
import { EmbeddedMapComponent } from '../../embedded_map';
|
||||
import { ExpandedRowPanel } from '../../stats_table/components/field_data_expanded_row/expanded_row_panel';
|
||||
|
||||
export const GeoPointContentWithMap: FC<{
|
||||
config: FieldVisConfig;
|
||||
|
@ -26,7 +26,7 @@ export const GeoPointContentWithMap: FC<{
|
|||
const { stats } = config;
|
||||
const [layerList, setLayerList] = useState<LayerDescriptor[]>([]);
|
||||
const {
|
||||
services: { maps: mapsPlugin },
|
||||
services: { maps: mapsPlugin, data },
|
||||
} = useDataVisualizerKibana();
|
||||
|
||||
// Update the layer list with updated geo points upon refresh
|
||||
|
@ -42,6 +42,7 @@ export const GeoPointContentWithMap: FC<{
|
|||
indexPatternId: indexPattern.id,
|
||||
geoFieldName: config.fieldName,
|
||||
geoFieldType: config.type as ES_GEO_FIELD_TYPE,
|
||||
filters: data.query.filterManager.getFilters() ?? [],
|
||||
query: {
|
||||
query: combinedQuery.searchString,
|
||||
language: combinedQuery.searchQueryLanguage,
|
||||
|
@ -57,19 +58,16 @@ export const GeoPointContentWithMap: FC<{
|
|||
}
|
||||
updateIndexPatternSearchLayer();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [indexPattern, combinedQuery, config, mapsPlugin]);
|
||||
}, [indexPattern, combinedQuery, config, mapsPlugin, data.query]);
|
||||
|
||||
if (stats?.examples === undefined) return null;
|
||||
return (
|
||||
<ExpandedRowContent dataTestSubj={'dataVisualizerIndexBasedMapContent'}>
|
||||
<DocumentStatsTable config={config} />
|
||||
|
||||
<EuiFlexItem style={{ maxWidth: '50%' }}>
|
||||
<ExamplesList examples={stats.examples} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem className={'dataVisualizerMapWrapper'}>
|
||||
<ExamplesList examples={stats.examples} />
|
||||
<ExpandedRowPanel className={'dvPanel__wrapper dvMap__wrapper'} grow={true}>
|
||||
<EmbeddedMapComponent layerList={layerList} />
|
||||
</EuiFlexItem>
|
||||
</ExpandedRowPanel>
|
||||
</ExpandedRowContent>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -22,15 +22,21 @@ import { FieldVisConfig } from '../stats_table/types';
|
|||
import { IndexPattern } from '../../../../../../../../src/plugins/data/common';
|
||||
import { CombinedQuery } from '../../../index_data_visualizer/types/combined_query';
|
||||
import { LoadingIndicator } from '../loading_indicator';
|
||||
import { IndexPatternField } from '../../../../../../../../src/plugins/data/common';
|
||||
|
||||
export const IndexBasedDataVisualizerExpandedRow = ({
|
||||
item,
|
||||
indexPattern,
|
||||
combinedQuery,
|
||||
onAddFilter,
|
||||
}: {
|
||||
item: FieldVisConfig;
|
||||
indexPattern: IndexPattern | undefined;
|
||||
combinedQuery: CombinedQuery;
|
||||
/**
|
||||
* Callback to add a filter to filter bar
|
||||
*/
|
||||
onAddFilter?: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
|
||||
}) => {
|
||||
const config = item;
|
||||
const { loading, type, existsInDocs, fieldName } = config;
|
||||
|
@ -42,7 +48,7 @@ export const IndexBasedDataVisualizerExpandedRow = ({
|
|||
|
||||
switch (type) {
|
||||
case JOB_FIELD_TYPES.NUMBER:
|
||||
return <NumberContent config={config} />;
|
||||
return <NumberContent config={config} onAddFilter={onAddFilter} />;
|
||||
|
||||
case JOB_FIELD_TYPES.BOOLEAN:
|
||||
return <BooleanContent config={config} />;
|
||||
|
@ -61,10 +67,10 @@ export const IndexBasedDataVisualizerExpandedRow = ({
|
|||
);
|
||||
|
||||
case JOB_FIELD_TYPES.IP:
|
||||
return <IpContent config={config} />;
|
||||
return <IpContent config={config} onAddFilter={onAddFilter} />;
|
||||
|
||||
case JOB_FIELD_TYPES.KEYWORD:
|
||||
return <KeywordContent config={config} />;
|
||||
return <KeywordContent config={config} onAddFilter={onAddFilter} />;
|
||||
|
||||
case JOB_FIELD_TYPES.TEXT:
|
||||
return <TextContent config={config} />;
|
||||
|
@ -75,10 +81,7 @@ export const IndexBasedDataVisualizerExpandedRow = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="dataVisualizerFieldExpandedRow"
|
||||
data-test-subj={`dataVisualizerFieldExpandedRow-${fieldName}`}
|
||||
>
|
||||
<div className="dvExpandedRow" data-test-subj={`dataVisualizerFieldExpandedRow-${fieldName}`}>
|
||||
{loading === true ? <LoadingIndicator /> : getCardContent()}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -28,12 +28,13 @@ export const FieldCountPanel: FC<Props> = ({
|
|||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="xs"
|
||||
style={{ marginLeft: 4 }}
|
||||
data-test-subj="dataVisualizerFieldCountPanel"
|
||||
responsive={false}
|
||||
className="dvFieldCount__panel"
|
||||
>
|
||||
<TotalFieldsCount fieldsCountStats={fieldsCountStats} />
|
||||
<MetricFieldsCount metricsStats={metricsStats} />
|
||||
<EuiFlexItem>
|
||||
<EuiFlexItem className={'dvFieldCount__item'}>
|
||||
<EuiSwitch
|
||||
data-test-subj="dataVisualizerShowEmptyFieldsSwitch"
|
||||
label={
|
||||
|
|
|
@ -20,13 +20,14 @@ import {
|
|||
|
||||
export function getActions(
|
||||
indexPattern: IndexPattern,
|
||||
services: DataVisualizerKibanaReactContextValue['services'],
|
||||
services: Partial<DataVisualizerKibanaReactContextValue['services']>,
|
||||
combinedQuery: CombinedQuery,
|
||||
actionFlyoutRef: MutableRefObject<(() => void | undefined) | undefined>
|
||||
): Array<Action<FieldVisConfig>> {
|
||||
const { lens: lensPlugin, indexPatternFieldEditor } = services;
|
||||
const { lens: lensPlugin, data } = services;
|
||||
|
||||
const actions: Array<Action<FieldVisConfig>> = [];
|
||||
const filters = data?.query.filterManager.getFilters() ?? [];
|
||||
|
||||
const refreshPage = () => {
|
||||
const refresh: Refresh = {
|
||||
|
@ -49,7 +50,7 @@ export function getActions(
|
|||
available: (item: FieldVisConfig) =>
|
||||
getCompatibleLensDataType(item.type) !== undefined && canUseLensEditor,
|
||||
onClick: (item: FieldVisConfig) => {
|
||||
const lensAttributes = getLensAttributes(indexPattern, combinedQuery, item);
|
||||
const lensAttributes = getLensAttributes(indexPattern, combinedQuery, filters, item);
|
||||
if (lensAttributes) {
|
||||
lensPlugin.navigateToPrefilledEditor({
|
||||
id: `dataVisualizer-${item.fieldName}`,
|
||||
|
@ -62,7 +63,7 @@ export function getActions(
|
|||
}
|
||||
|
||||
// Allow to edit index pattern field
|
||||
if (indexPatternFieldEditor?.userPermissions.editIndexPattern()) {
|
||||
if (services.indexPatternFieldEditor?.userPermissions.editIndexPattern()) {
|
||||
actions.push({
|
||||
name: i18n.translate('xpack.dataVisualizer.index.dataGrid.editIndexPatternFieldTitle', {
|
||||
defaultMessage: 'Edit index pattern field',
|
||||
|
@ -76,7 +77,7 @@ export function getActions(
|
|||
type: 'icon',
|
||||
icon: 'indexEdit',
|
||||
onClick: (item: FieldVisConfig) => {
|
||||
actionFlyoutRef.current = indexPatternFieldEditor?.openEditor({
|
||||
actionFlyoutRef.current = services.indexPatternFieldEditor?.openEditor({
|
||||
ctx: { indexPattern },
|
||||
fieldName: item.fieldName,
|
||||
onSave: refreshPage,
|
||||
|
@ -100,7 +101,7 @@ export function getActions(
|
|||
return item.deletable === true;
|
||||
},
|
||||
onClick: (item: FieldVisConfig) => {
|
||||
actionFlyoutRef.current = indexPatternFieldEditor?.openDeleteModal({
|
||||
actionFlyoutRef.current = services.indexPatternFieldEditor?.openDeleteModal({
|
||||
ctx: { indexPattern },
|
||||
fieldName: item.fieldName!,
|
||||
onDelete: refreshPage,
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import type { IndexPattern } from '../../../../../../../../../src/plugins/data/common';
|
||||
import type { CombinedQuery } from '../../../../index_data_visualizer/types/combined_query';
|
||||
import type {
|
||||
|
@ -15,6 +16,7 @@ import type {
|
|||
} from '../../../../../../../lens/public';
|
||||
import { FieldVisConfig } from '../../stats_table/types';
|
||||
import { JOB_FIELD_TYPES } from '../../../../../../common';
|
||||
|
||||
interface ColumnsAndLayer {
|
||||
columns: Record<string, IndexPatternColumn>;
|
||||
layer: XYLayerConfig;
|
||||
|
@ -241,6 +243,7 @@ function getColumnsAndLayer(
|
|||
export function getLensAttributes(
|
||||
defaultIndexPattern: IndexPattern | undefined,
|
||||
combinedQuery: CombinedQuery,
|
||||
filters: Filter[],
|
||||
item: FieldVisConfig
|
||||
): TypedLensByValueInput['attributes'] | 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 },
|
||||
visualization: {
|
||||
axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true },
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import React from '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';
|
||||
|
||||
export const FileBasedNumberContentPreview = ({ config }: { config: FileBasedFieldVisConfig }) => {
|
||||
|
@ -23,28 +23,34 @@ export const FileBasedNumberContentPreview = ({ config }: { config: FileBasedFie
|
|||
<EuiFlexGroup direction={'column'} gutterSize={'xs'}>
|
||||
<EuiFlexGroup gutterSize="xs">
|
||||
<EuiFlexItem>
|
||||
<b>
|
||||
<EuiText size={'xs'}>
|
||||
<FormattedMessage id="xpack.dataVisualizer.fieldStats.minTitle" defaultMessage="min" />
|
||||
</b>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<b>
|
||||
<EuiText size={'xs'}>
|
||||
<FormattedMessage
|
||||
id="xpack.dataVisualizer.fieldStats.medianTitle"
|
||||
defaultMessage="median"
|
||||
/>
|
||||
</b>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<b>
|
||||
<EuiText size={'xs'}>
|
||||
<FormattedMessage id="xpack.dataVisualizer.fieldStats.maxTitle" defaultMessage="max" />
|
||||
</b>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup gutterSize="xs">
|
||||
<EuiFlexItem>{stats.min}</EuiFlexItem>
|
||||
<EuiFlexItem>{stats.median}</EuiFlexItem>
|
||||
<EuiFlexItem>{stats.max}</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size={'xs'}>{stats.min}</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size={'xs'}>{stats.median}</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size={'xs'}>{stats.max}</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
exports[`FieldTypeIcon render component when type matches a field type 1`] = `
|
||||
<EuiToolTip
|
||||
anchorClassName="dvFieldTypeIcon__anchor"
|
||||
content="keyword type"
|
||||
delay="regular"
|
||||
display="inlineBlock"
|
||||
|
@ -9,8 +10,7 @@ exports[`FieldTypeIcon render component when type matches a field type 1`] = `
|
|||
>
|
||||
<FieldTypeIconContainer
|
||||
ariaLabel="keyword type"
|
||||
color="euiColorVis0"
|
||||
iconType="tokenText"
|
||||
iconType="tokenKeyword"
|
||||
needsAria={false}
|
||||
/>
|
||||
</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(
|
||||
<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);
|
||||
|
||||
container.simulate('mouseover');
|
||||
typeIconComponent.simulate('mouseover');
|
||||
|
||||
// Run the timers so the EuiTooltip will be visible
|
||||
jest.runAllTimers();
|
||||
|
@ -38,7 +37,7 @@ describe('FieldTypeIcon', () => {
|
|||
typeIconComponent.update();
|
||||
expect(typeIconComponent.find('EuiToolTip').children()).toHaveLength(2);
|
||||
|
||||
container.simulate('mouseout');
|
||||
typeIconComponent.simulate('mouseout');
|
||||
|
||||
// Run the timers so the EuiTooltip will be hidden again
|
||||
jest.runAllTimers();
|
||||
|
|
|
@ -6,91 +6,62 @@
|
|||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import { EuiToken, EuiToolTip } from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { getJobTypeAriaLabel } from '../../util/field_types_utils';
|
||||
import { JOB_FIELD_TYPES } from '../../../../../common';
|
||||
import type { JobFieldType } from '../../../../../common';
|
||||
import './_index.scss';
|
||||
|
||||
interface FieldTypeIconProps {
|
||||
tooltipEnabled: boolean;
|
||||
type: JobFieldType;
|
||||
fieldName?: string;
|
||||
needsAria: boolean;
|
||||
}
|
||||
|
||||
interface FieldTypeIconContainerProps {
|
||||
ariaLabel: string | null;
|
||||
iconType: string;
|
||||
color: string;
|
||||
color?: string;
|
||||
needsAria: boolean;
|
||||
[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> = ({
|
||||
tooltipEnabled = false,
|
||||
type,
|
||||
fieldName,
|
||||
needsAria = true,
|
||||
}) => {
|
||||
const ariaLabel = getJobTypeAriaLabel(type);
|
||||
|
||||
let iconType = 'questionInCircle';
|
||||
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,
|
||||
};
|
||||
const token = typeToEuiIconMap[type] || defaultIcon;
|
||||
const containerProps = { ...token, ariaLabel, needsAria };
|
||||
|
||||
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 (
|
||||
<EuiToolTip
|
||||
position="left"
|
||||
|
@ -98,6 +69,7 @@ export const FieldTypeIcon: FC<FieldTypeIconProps> = ({
|
|||
defaultMessage: '{type} type',
|
||||
values: { type },
|
||||
})}
|
||||
anchorClassName="dvFieldTypeIcon__anchor"
|
||||
>
|
||||
<FieldTypeIconContainer {...containerProps} />
|
||||
</EuiToolTip>
|
||||
|
@ -122,12 +94,15 @@ const FieldTypeIconContainer: FC<FieldTypeIconContainerProps> = ({
|
|||
if (needsAria && ariaLabel) {
|
||||
wrapperProps['aria-label'] = ariaLabel;
|
||||
}
|
||||
|
||||
return (
|
||||
<span data-test-subj="fieldTypeIcon" {...rest}>
|
||||
<span {...wrapperProps}>
|
||||
<EuiToken iconType={iconType} shape="square" size="s" color={color} />
|
||||
</span>
|
||||
</span>
|
||||
<EuiToken
|
||||
iconType={iconType}
|
||||
color={color}
|
||||
shape="square"
|
||||
size="s"
|
||||
data-test-subj="fieldTypeIcon"
|
||||
{...wrapperProps}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -14,7 +14,7 @@ import type {
|
|||
FileBasedUnknownFieldVisConfig,
|
||||
} from '../stats_table/types/field_vis_config';
|
||||
import { FieldTypeIcon } from '../field_type_icon';
|
||||
import { JOB_FIELD_TYPES_OPTIONS } from '../../../../../common';
|
||||
import { jobTypeLabels } from '../../util/field_types_utils';
|
||||
|
||||
interface Props {
|
||||
fields: Array<FileBasedFieldVisConfig | FileBasedUnknownFieldVisConfig>;
|
||||
|
@ -39,27 +39,18 @@ export const DataVisualizerFieldTypesFilter: FC<Props> = ({
|
|||
const fieldTypesTracker = new Set();
|
||||
const fieldTypes: Option[] = [];
|
||||
fields.forEach(({ type }) => {
|
||||
if (
|
||||
type !== undefined &&
|
||||
!fieldTypesTracker.has(type) &&
|
||||
JOB_FIELD_TYPES_OPTIONS[type] !== undefined
|
||||
) {
|
||||
const item = JOB_FIELD_TYPES_OPTIONS[type];
|
||||
if (type !== undefined && !fieldTypesTracker.has(type) && jobTypeLabels[type] !== undefined) {
|
||||
const label = jobTypeLabels[type];
|
||||
|
||||
fieldTypesTracker.add(type);
|
||||
fieldTypes.push({
|
||||
value: type,
|
||||
name: (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={true}> {item.name}</EuiFlexItem>
|
||||
<EuiFlexItem grow={true}> {label}</EuiFlexItem>
|
||||
{type && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<FieldTypeIcon
|
||||
type={type}
|
||||
fieldName={item.name}
|
||||
tooltipEnabled={false}
|
||||
needsAria={true}
|
||||
/>
|
||||
<FieldTypeIcon type={type} tooltipEnabled={false} needsAria={true} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -25,7 +25,7 @@ interface Props {
|
|||
|
||||
export const getDefaultDataVisualizerListState = (): DataVisualizerTableState => ({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
pageSize: 25,
|
||||
sortField: 'fieldName',
|
||||
sortDirection: 'asc',
|
||||
visibleFieldTypes: [],
|
||||
|
|
|
@ -98,7 +98,7 @@ export const MultiSelectPicker: FC<{
|
|||
);
|
||||
|
||||
return (
|
||||
<EuiFilterGroup data-test-subj={dataTestSubj}>
|
||||
<EuiFilterGroup data-test-subj={dataTestSubj} style={{ marginLeft: 8 }}>
|
||||
<EuiPopover
|
||||
ownFocus
|
||||
data-test-subj={`${dataTestSubj}-popover`}
|
||||
|
|
|
@ -6,19 +6,16 @@
|
|||
*/
|
||||
|
||||
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';
|
||||
|
||||
export const NotInDocsContent: FC = () => (
|
||||
<Fragment>
|
||||
<EuiSpacer size="xxl" />
|
||||
<EuiText textAlign="center">
|
||||
<EuiIcon type="alert" />
|
||||
</EuiText>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText textAlign="center">
|
||||
<EuiText textAlign="center" size={'xs'}>
|
||||
<FormattedMessage
|
||||
id="xpack.dataVisualizer.dataGrid.field.fieldNotInDocsLabel"
|
||||
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_data_row/index';
|
||||
|
||||
.dataVisualizerFieldExpandedRow {
|
||||
$panelWidthS: #{'max(20%, 225px)'};
|
||||
$panelWidthL: #{'max(40%, 450px)'};
|
||||
|
||||
.dvExpandedRow {
|
||||
padding-left: $euiSize * 4;
|
||||
width: 100%;
|
||||
|
||||
.fieldDataCard__valuesTitle {
|
||||
.dvExpandedRow__fieldHeader {
|
||||
text-transform: uppercase;
|
||||
text-align: left;
|
||||
color: $euiColorDarkShade;
|
||||
font-weight: bold;
|
||||
padding-bottom: $euiSizeS;
|
||||
}
|
||||
|
||||
.fieldDataCard__codeContent {
|
||||
@include euiCodeFont;
|
||||
}
|
||||
}
|
||||
|
||||
.dataVisualizer {
|
||||
.euiTableRow > .euiTableRowCell {
|
||||
border-bottom: 0;
|
||||
border-top: $euiBorderThin;
|
||||
|
||||
}
|
||||
.euiTableRow-isExpandedRow {
|
||||
|
||||
.euiTableRowCell {
|
||||
background-color: $euiColorEmptyShade !important;
|
||||
border-top: 0;
|
||||
border-bottom: $euiBorderThin;
|
||||
&:hover {
|
||||
background-color: $euiColorEmptyShade !important;
|
||||
}
|
||||
@include euiBreakpoint('m', 'l', 'xl') {
|
||||
.dvTable {
|
||||
.columnHeader__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
.dataVisualizerSummaryTable {
|
||||
max-width: 350px;
|
||||
min-width: 250px;
|
||||
|
||||
.columnHeader__icon {
|
||||
padding-right: $euiSizeXS;
|
||||
}
|
||||
|
||||
.euiTableRow > .euiTableRowCell {
|
||||
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';
|
||||
|
||||
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}
|
||||
</EuiText>
|
||||
);
|
||||
|
|
|
@ -1,3 +1,12 @@
|
|||
.dataVisualizerFieldCountContainer {
|
||||
max-width: 300px;
|
||||
.dvFieldCount__panel {
|
||||
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
|
||||
gutterSize="s"
|
||||
alignItems="center"
|
||||
className="dataVisualizerFieldCountContainer"
|
||||
className="dvFieldCount__item"
|
||||
data-test-subj="dataVisualizerMetricFieldsSummary"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
|
|
|
@ -30,8 +30,9 @@ export const TotalFieldsCount: FC<TotalFieldsCountProps> = ({ fieldsCountStats }
|
|||
<EuiFlexGroup
|
||||
gutterSize="s"
|
||||
alignItems="center"
|
||||
className="dataVisualizerFieldCountContainer"
|
||||
className="dvFieldCount__item"
|
||||
data-test-subj="dataVisualizerFieldsSummary"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
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 { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
@ -18,6 +18,7 @@ import { roundToDecimalPlace } from '../../../utils';
|
|||
import { useDataVizChartTheme } from '../../hooks';
|
||||
import { DocumentStatsTable } from './document_stats';
|
||||
import { ExpandedRowContent } from './expanded_row_content';
|
||||
import { ExpandedRowPanel } from './expanded_row_panel';
|
||||
|
||||
function getPercentLabel(value: number): string {
|
||||
if (value === 0) {
|
||||
|
@ -35,7 +36,7 @@ function getFormattedValue(value: number, totalCount: number): string {
|
|||
return `${value} (${getPercentLabel(percentage)})`;
|
||||
}
|
||||
|
||||
const BOOLEAN_DISTRIBUTION_CHART_HEIGHT = 100;
|
||||
const BOOLEAN_DISTRIBUTION_CHART_HEIGHT = 70;
|
||||
|
||||
export const BooleanContent: FC<FieldDataRowProps> = ({ config }) => {
|
||||
const fieldFormat = 'fieldFormat' in config ? config.fieldFormat : undefined;
|
||||
|
@ -68,9 +69,11 @@ export const BooleanContent: FC<FieldDataRowProps> = ({ config }) => {
|
|||
];
|
||||
const summaryTableColumns = [
|
||||
{
|
||||
field: 'function',
|
||||
name: '',
|
||||
render: (summaryItem: { display: ReactNode }) => summaryItem.display,
|
||||
width: '75px',
|
||||
render: (_: string, summaryItem: { display: ReactNode }) => summaryItem.display,
|
||||
width: '25px',
|
||||
align: RIGHT_ALIGNMENT as HorizontalAlignment,
|
||||
},
|
||||
{
|
||||
field: 'value',
|
||||
|
@ -90,18 +93,18 @@ export const BooleanContent: FC<FieldDataRowProps> = ({ config }) => {
|
|||
<ExpandedRowContent dataTestSubj={'dataVisualizerBooleanContent'}>
|
||||
<DocumentStatsTable config={config} />
|
||||
|
||||
<EuiFlexItem className={'dataVisualizerSummaryTableWrapper'}>
|
||||
<ExpandedRowPanel className={'dvSummaryTable__wrapper dvPanel__wrapper'}>
|
||||
<ExpandedRowFieldHeader>{summaryTableTitle}</ExpandedRowFieldHeader>
|
||||
<EuiBasicTable
|
||||
className={'dataVisualizerSummaryTable'}
|
||||
className={'dvSummaryTable'}
|
||||
compressed
|
||||
items={summaryTableItems}
|
||||
columns={summaryTableColumns}
|
||||
tableCaption={summaryTableTitle}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</ExpandedRowPanel>
|
||||
|
||||
<EuiFlexItem>
|
||||
<ExpandedRowPanel className={'dvPanel__wrapper dvPanel--uniform'}>
|
||||
<ExpandedRowFieldHeader>
|
||||
<FormattedMessage
|
||||
id="xpack.dataVisualizer.dataGrid.field.cardBoolean.valuesLabel"
|
||||
|
@ -139,7 +142,7 @@ export const BooleanContent: FC<FieldDataRowProps> = ({ config }) => {
|
|||
yScaleType="linear"
|
||||
/>
|
||||
</Chart>
|
||||
</EuiFlexItem>
|
||||
</ExpandedRowPanel>
|
||||
</ExpandedRowContent>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
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 { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
|
@ -20,6 +20,7 @@ import {
|
|||
import { EMSTermJoinConfig } from '../../../../../../../../maps/public';
|
||||
import { EmbeddedMapComponent } from '../../../embedded_map';
|
||||
import { FieldVisStats } from '../../../../../../../common/types';
|
||||
import { ExpandedRowPanel } from './expanded_row_panel';
|
||||
|
||||
export const getChoroplethTopValuesLayer = (
|
||||
fieldName: string,
|
||||
|
@ -104,14 +105,19 @@ export const ChoroplethMap: FC<Props> = ({ stats, suggestion }) => {
|
|||
);
|
||||
|
||||
return (
|
||||
<EuiFlexItem data-test-subj={'fileDataVisualizerChoroplethMapTopValues'}>
|
||||
<div style={{ width: '100%', minHeight: 300 }}>
|
||||
<ExpandedRowPanel
|
||||
dataTestSubj={'fileDataVisualizerChoroplethMapTopValues'}
|
||||
className={'dvPanel__wrapper'}
|
||||
grow={true}
|
||||
>
|
||||
<div className={'dvMap__wrapper'}>
|
||||
<EmbeddedMapComponent layerList={layerList} />
|
||||
</div>
|
||||
|
||||
{isTopValuesSampled === true && (
|
||||
<>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiText size="xs" textAlign={'left'}>
|
||||
<div>
|
||||
<EuiSpacer size={'s'} />
|
||||
<EuiText size="xs" textAlign={'center'}>
|
||||
<FormattedMessage
|
||||
id="xpack.dataVisualizer.dataGrid.fieldExpandedRow.choroplethMapTopValues.calculatedFromSampleDescription"
|
||||
defaultMessage="Calculated from sample of {topValuesSamplerShardSize} documents per shard"
|
||||
|
@ -120,8 +126,8 @@ export const ChoroplethMap: FC<Props> = ({ stats, suggestion }) => {
|
|||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</ExpandedRowPanel>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,16 +6,18 @@
|
|||
*/
|
||||
|
||||
import React, { FC, ReactNode } from 'react';
|
||||
import { EuiBasicTable, EuiFlexItem } from '@elastic/eui';
|
||||
import { EuiBasicTable, HorizontalAlignment } from '@elastic/eui';
|
||||
// @ts-ignore
|
||||
import { formatDate } from '@elastic/eui/lib/services/format';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { RIGHT_ALIGNMENT } from '@elastic/eui';
|
||||
import type { FieldDataRowProps } from '../../types/field_data_row';
|
||||
import { ExpandedRowFieldHeader } from '../expanded_row_field_header';
|
||||
import { DocumentStatsTable } from './document_stats';
|
||||
import { ExpandedRowContent } from './expanded_row_content';
|
||||
import { ExpandedRowPanel } from './expanded_row_panel';
|
||||
const TIME_FORMAT = 'MMM D YYYY, HH:mm:ss.SSS';
|
||||
interface SummaryTableItem {
|
||||
function: string;
|
||||
|
@ -60,8 +62,10 @@ export const DateContent: FC<FieldDataRowProps> = ({ config }) => {
|
|||
const summaryTableColumns = [
|
||||
{
|
||||
name: '',
|
||||
render: (summaryItem: { display: ReactNode }) => summaryItem.display,
|
||||
width: '75px',
|
||||
field: 'function',
|
||||
render: (func: string, summaryItem: { display: ReactNode }) => summaryItem.display,
|
||||
width: '70px',
|
||||
align: RIGHT_ALIGNMENT as HorizontalAlignment,
|
||||
},
|
||||
{
|
||||
field: 'value',
|
||||
|
@ -73,10 +77,10 @@ export const DateContent: FC<FieldDataRowProps> = ({ config }) => {
|
|||
return (
|
||||
<ExpandedRowContent dataTestSubj={'dataVisualizerDateContent'}>
|
||||
<DocumentStatsTable config={config} />
|
||||
<EuiFlexItem className={'dataVisualizerSummaryTableWrapper'}>
|
||||
<ExpandedRowPanel className={'dvSummaryTable__wrapper dvPanel__wrapper'}>
|
||||
<ExpandedRowFieldHeader>{summaryTableTitle}</ExpandedRowFieldHeader>
|
||||
<EuiBasicTable<SummaryTableItem>
|
||||
className={'dataVisualizerSummaryTable'}
|
||||
className={'dvSummaryTable'}
|
||||
data-test-subj={'dataVisualizerDateSummaryTable'}
|
||||
compressed
|
||||
items={summaryTableItems}
|
||||
|
@ -84,7 +88,7 @@ export const DateContent: FC<FieldDataRowProps> = ({ config }) => {
|
|||
tableCaption={summaryTableTitle}
|
||||
tableLayout="auto"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</ExpandedRowPanel>
|
||||
</ExpandedRowContent>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,16 +8,19 @@
|
|||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { FC, ReactNode } from 'react';
|
||||
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 { FieldDataRowProps } from '../../types';
|
||||
import { roundToDecimalPlace } from '../../../utils';
|
||||
import { ExpandedRowPanel } from './expanded_row_panel';
|
||||
|
||||
const metaTableColumns = [
|
||||
{
|
||||
field: 'function',
|
||||
name: '',
|
||||
render: (metaItem: { display: ReactNode }) => metaItem.display,
|
||||
width: '75px',
|
||||
render: (_: string, metaItem: { display: ReactNode }) => metaItem.display,
|
||||
width: '25px',
|
||||
align: RIGHT_ALIGNMENT as HorizontalAlignment,
|
||||
},
|
||||
{
|
||||
field: 'value',
|
||||
|
@ -76,18 +79,18 @@ export const DocumentStatsTable: FC<FieldDataRowProps> = ({ config }) => {
|
|||
];
|
||||
|
||||
return (
|
||||
<EuiFlexItem
|
||||
data-test-subj={'dataVisualizerDocumentStatsContent'}
|
||||
className={'dataVisualizerSummaryTableWrapper'}
|
||||
<ExpandedRowPanel
|
||||
dataTestSubj={'dataVisualizerDocumentStatsContent'}
|
||||
className={'dvSummaryTable__wrapper dvPanel__wrapper'}
|
||||
>
|
||||
<ExpandedRowFieldHeader>{metaTableTitle}</ExpandedRowFieldHeader>
|
||||
<EuiBasicTable
|
||||
className={'dataVisualizerSummaryTable'}
|
||||
className={'dvSummaryTable'}
|
||||
compressed
|
||||
items={metaTableItems}
|
||||
columns={metaTableColumns}
|
||||
tableCaption={metaTableTitle}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</ExpandedRowPanel>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import React, { FC, ReactNode } from 'react';
|
||||
import { EuiFlexGroup } from '@elastic/eui';
|
||||
import { EuiFlexGrid } from '@elastic/eui';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
|
@ -14,12 +14,8 @@ interface Props {
|
|||
}
|
||||
export const ExpandedRowContent: FC<Props> = ({ children, dataTestSubj }) => {
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
data-test-subj={dataTestSubj}
|
||||
gutterSize={'xl'}
|
||||
className={'dataVisualizerExpandedRow'}
|
||||
>
|
||||
<EuiFlexGrid data-test-subj={dataTestSubj} gutterSize={'s'}>
|
||||
{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 { ExpandedRowContent } from './expanded_row_content';
|
||||
|
||||
export const IpContent: FC<FieldDataRowProps> = ({ config }) => {
|
||||
export const IpContent: FC<FieldDataRowProps> = ({ config, onAddFilter }) => {
|
||||
const { stats } = config;
|
||||
if (stats === undefined) return null;
|
||||
const { count, sampleCount, cardinality } = stats;
|
||||
|
@ -21,7 +21,12 @@ export const IpContent: FC<FieldDataRowProps> = ({ config }) => {
|
|||
return (
|
||||
<ExpandedRowContent dataTestSubj={'dataVisualizerIPContent'}>
|
||||
<DocumentStatsTable config={config} />
|
||||
<TopValues stats={stats} fieldFormat={fieldFormat} barColor="secondary" />
|
||||
<TopValues
|
||||
stats={stats}
|
||||
fieldFormat={fieldFormat}
|
||||
barColor="secondary"
|
||||
onAddFilter={onAddFilter}
|
||||
/>
|
||||
</ExpandedRowContent>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -14,7 +14,7 @@ import { DocumentStatsTable } from './document_stats';
|
|||
import { ExpandedRowContent } from './expanded_row_content';
|
||||
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 { stats, fieldName } = config;
|
||||
const fieldFormat = 'fieldFormat' in config ? config.fieldFormat : undefined;
|
||||
|
@ -44,7 +44,12 @@ export const KeywordContent: FC<FieldDataRowProps> = ({ config }) => {
|
|||
return (
|
||||
<ExpandedRowContent dataTestSubj={'dataVisualizerKeywordContent'}>
|
||||
<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} />}
|
||||
</ExpandedRowContent>
|
||||
);
|
||||
|
|
|
@ -6,7 +6,13 @@
|
|||
*/
|
||||
|
||||
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 { i18n } from '@kbn/i18n';
|
||||
|
@ -21,8 +27,9 @@ import { TopValues } from '../../../top_values';
|
|||
import { ExpandedRowFieldHeader } from '../expanded_row_field_header';
|
||||
import { DocumentStatsTable } from './document_stats';
|
||||
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;
|
||||
|
||||
interface SummaryTableItem {
|
||||
|
@ -31,7 +38,7 @@ interface SummaryTableItem {
|
|||
value: number | string | undefined | null;
|
||||
}
|
||||
|
||||
export const NumberContent: FC<FieldDataRowProps> = ({ config }) => {
|
||||
export const NumberContent: FC<FieldDataRowProps> = ({ config, onAddFilter }) => {
|
||||
const { stats } = config;
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -83,7 +90,8 @@ export const NumberContent: FC<FieldDataRowProps> = ({ config }) => {
|
|||
{
|
||||
name: '',
|
||||
render: (summaryItem: { display: ReactNode }) => summaryItem.display,
|
||||
width: '75px',
|
||||
width: '25px',
|
||||
align: RIGHT_ALIGNMENT as HorizontalAlignment,
|
||||
},
|
||||
{
|
||||
field: 'value',
|
||||
|
@ -101,23 +109,33 @@ export const NumberContent: FC<FieldDataRowProps> = ({ config }) => {
|
|||
return (
|
||||
<ExpandedRowContent dataTestSubj={'dataVisualizerNumberContent'}>
|
||||
<DocumentStatsTable config={config} />
|
||||
<EuiFlexItem className={'dataVisualizerSummaryTableWrapper'}>
|
||||
<ExpandedRowPanel className={'dvSummaryTable__wrapper dvPanel__wrapper'} grow={1}>
|
||||
<ExpandedRowFieldHeader>{summaryTableTitle}</ExpandedRowFieldHeader>
|
||||
<EuiBasicTable<SummaryTableItem>
|
||||
className={'dataVisualizerSummaryTable'}
|
||||
className={'dvSummaryTable'}
|
||||
compressed
|
||||
items={summaryTableItems}
|
||||
columns={summaryTableColumns}
|
||||
tableCaption={summaryTableTitle}
|
||||
data-test-subj={'dataVisualizerNumberSummaryTable'}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</ExpandedRowPanel>
|
||||
|
||||
{stats && (
|
||||
<TopValues stats={stats} fieldFormat={fieldFormat} barColor="secondary" compressed={true} />
|
||||
<TopValues
|
||||
stats={stats}
|
||||
fieldFormat={fieldFormat}
|
||||
barColor="secondary"
|
||||
compressed={true}
|
||||
onAddFilter={onAddFilter}
|
||||
/>
|
||||
)}
|
||||
{distribution && (
|
||||
<EuiFlexItem data-test-subj={'dataVisualizerFieldDataMetricDistribution'}>
|
||||
<ExpandedRowPanel
|
||||
dataTestSubj={'dataVisualizerFieldDataMetricDistribution'}
|
||||
className="dvPanel__wrapper"
|
||||
grow={false}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ExpandedRowFieldHeader>
|
||||
<FormattedMessage
|
||||
|
@ -136,7 +154,7 @@ export const NumberContent: FC<FieldDataRowProps> = ({ config }) => {
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs">
|
||||
<EuiText size="xs" textAlign={'center'}>
|
||||
<FormattedMessage
|
||||
id="xpack.dataVisualizer.dataGrid.fieldExpandedRow.numberContent.displayingPercentilesLabel"
|
||||
defaultMessage="Displaying {minPercent} - {maxPercent} percentiles"
|
||||
|
@ -147,7 +165,7 @@ export const NumberContent: FC<FieldDataRowProps> = ({ config }) => {
|
|||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexItem>
|
||||
</ExpandedRowPanel>
|
||||
)}
|
||||
</ExpandedRowContent>
|
||||
);
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import { EuiFlexItem } from '@elastic/eui';
|
||||
import type { FieldDataRowProps } from '../../types/field_data_row';
|
||||
import { ExamplesList } from '../../../examples_list';
|
||||
import { DocumentStatsTable } from './document_stats';
|
||||
|
@ -15,14 +14,12 @@ import { ExpandedRowContent } from './expanded_row_content';
|
|||
export const OtherContent: FC<FieldDataRowProps> = ({ config }) => {
|
||||
const { stats } = config;
|
||||
if (stats === undefined) return null;
|
||||
return (
|
||||
return stats.count === undefined ? (
|
||||
<>{Array.isArray(stats.examples) && <ExamplesList examples={stats.examples} />}</>
|
||||
) : (
|
||||
<ExpandedRowContent dataTestSubj={'dataVisualizerOtherContent'}>
|
||||
<DocumentStatsTable config={config} />
|
||||
{Array.isArray(stats.examples) && (
|
||||
<EuiFlexItem>
|
||||
<ExamplesList examples={stats.examples} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{Array.isArray(stats.examples) && <ExamplesList examples={stats.examples} />}
|
||||
</ExpandedRowContent>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
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 { i18n } from '@kbn/i18n';
|
||||
|
@ -26,7 +26,7 @@ export const TextContent: FC<FieldDataRowProps> = ({ config }) => {
|
|||
|
||||
return (
|
||||
<ExpandedRowContent dataTestSubj={'dataVisualizerTextContent'}>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexItem grow={false} className="dvText__wrapper">
|
||||
{numExamples > 0 && <ExamplesList examples={examples} />}
|
||||
{numExamples === 0 && (
|
||||
<Fragment>
|
||||
|
@ -44,7 +44,7 @@ export const TextContent: FC<FieldDataRowProps> = ({ config }) => {
|
|||
id="xpack.dataVisualizer.dataGrid.fieldText.fieldNotPresentDescription"
|
||||
defaultMessage="This field was not present in the {sourceParam} field of documents queried."
|
||||
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"
|
||||
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={{
|
||||
copyToParam: <span className="fieldDataCard__codeContent">copy_to</span>,
|
||||
sourceParam: <span className="fieldDataCard__codeContent">_source</span>,
|
||||
includesParam: <span className="fieldDataCard__codeContent">includes</span>,
|
||||
excludesParam: <span className="fieldDataCard__codeContent">excludes</span>,
|
||||
copyToParam: <span className="dvExpandedRow__codeContent">copy_to</span>,
|
||||
sourceParam: <span className="dvExpandedRow__codeContent">_source</span>,
|
||||
includesParam: <span className="dvExpandedRow__codeContent">includes</span>,
|
||||
excludesParam: <span className="dvExpandedRow__codeContent">excludes</span>,
|
||||
}}
|
||||
/>
|
||||
</EuiCallOut>
|
||||
|
|
|
@ -1,19 +1,22 @@
|
|||
.dataGridChart__histogram {
|
||||
width: 100%;
|
||||
height: $euiSizeXL + $euiSizeXXL;
|
||||
}
|
||||
|
||||
.dataGridChart__column-chart {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dataGridChart__legend {
|
||||
@include euiTextTruncate;
|
||||
@include euiFontSizeXS;
|
||||
|
||||
color: $euiColorMediumShade;
|
||||
display: block;
|
||||
overflow-x: hidden;
|
||||
margin: $euiSizeXS 0 0 0;
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
text-align: left;
|
||||
line-height: 1.1;
|
||||
font-size: #{$euiFontSizeL / 2}; // 10px
|
||||
}
|
||||
|
||||
.dataGridChart__legend--numeric {
|
||||
|
@ -21,9 +24,7 @@
|
|||
}
|
||||
|
||||
.dataGridChart__legendBoolean {
|
||||
width: 100%;
|
||||
min-width: $euiButtonMinWidth;
|
||||
td { text-align: center }
|
||||
width: #{$euiSizeXS * 2.5} // 10px
|
||||
}
|
||||
|
||||
/* Override to align column header to bottom of cell when no chart is available */
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import React, { FC } from 'react';
|
||||
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 './column_chart.scss';
|
||||
|
@ -25,22 +25,9 @@ interface Props {
|
|||
maxChartColumns?: number;
|
||||
}
|
||||
|
||||
const columnChartTheme = {
|
||||
background: { color: 'transparent' },
|
||||
chartMargins: {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 1,
|
||||
},
|
||||
chartPaddings: {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
scales: { barsPadding: 0.1 },
|
||||
};
|
||||
const zeroSize = { bottom: 0, left: 0, right: 0, top: 0 };
|
||||
const size = { width: 100, height: 10 };
|
||||
|
||||
export const ColumnChart: FC<Props> = ({
|
||||
chartData,
|
||||
columnType,
|
||||
|
@ -48,26 +35,34 @@ export const ColumnChart: FC<Props> = ({
|
|||
hideLabel,
|
||||
maxChartColumns,
|
||||
}) => {
|
||||
const { data, legendText, xScaleType } = useColumnChart(chartData, columnType, maxChartColumns);
|
||||
const { data, legendText } = useColumnChart(chartData, columnType, maxChartColumns);
|
||||
|
||||
return (
|
||||
<div data-test-subj={dataTestSubj}>
|
||||
{!isUnsupportedChartData(chartData) && data.length > 0 && (
|
||||
<div className="dataGridChart__histogram" data-test-subj={`${dataTestSubj}-histogram`}>
|
||||
<Chart>
|
||||
<Settings theme={columnChartTheme} />
|
||||
<BarSeries
|
||||
id="histogram"
|
||||
name="count"
|
||||
xScaleType={xScaleType}
|
||||
yScaleType="linear"
|
||||
xAccessor={'key_as_string'}
|
||||
yAccessors={['doc_count']}
|
||||
styleAccessor={(d) => d.datum.color}
|
||||
data={data}
|
||||
/>
|
||||
</Chart>
|
||||
</div>
|
||||
<Chart size={size}>
|
||||
<Settings
|
||||
xDomain={{ min: 0, max: 9 }}
|
||||
theme={{ chartMargins: zeroSize, chartPaddings: zeroSize }}
|
||||
/>
|
||||
<Axis
|
||||
id="bottom"
|
||||
position={Position.Bottom}
|
||||
tickFormat={(idx) => {
|
||||
return `${data[idx]?.key_as_string ?? ''}`;
|
||||
}}
|
||||
hide
|
||||
/>
|
||||
<BarSeries
|
||||
id={'count'}
|
||||
xScaleType={ScaleType.Linear}
|
||||
yScaleType={ScaleType.Linear}
|
||||
xAccessor="x"
|
||||
yAccessors={['doc_count']}
|
||||
data={data}
|
||||
styleAccessor={(d) => d.datum.color}
|
||||
/>
|
||||
</Chart>
|
||||
)}
|
||||
<div
|
||||
className={classNames('dataGridChart__legend', {
|
||||
|
|
|
@ -5,20 +5,21 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui';
|
||||
import { EuiIcon, EuiText } from '@elastic/eui';
|
||||
|
||||
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;
|
||||
return (
|
||||
<EuiFlexGroup alignItems={'center'}>
|
||||
<EuiFlexItem className={'dataVisualizerColumnHeaderIcon'}>
|
||||
<EuiIcon type="database" size={'s'} />
|
||||
</EuiFlexItem>
|
||||
<EuiText size={'s'}>
|
||||
<b>{cardinality}</b>
|
||||
</EuiText>
|
||||
</EuiFlexGroup>
|
||||
<>
|
||||
{showIcon ? <EuiIcon type="database" size={'m'} className={'columnHeader__icon'} /> : null}
|
||||
<EuiText size={'xs'}>{cardinality}</EuiText>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,29 +5,36 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui';
|
||||
import { EuiIcon, EuiText } from '@elastic/eui';
|
||||
|
||||
import React from 'react';
|
||||
import type { FieldDataRowProps } from '../../types/field_data_row';
|
||||
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;
|
||||
if (stats === undefined) return null;
|
||||
|
||||
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 (
|
||||
<EuiFlexGroup alignItems={'center'}>
|
||||
<EuiFlexItem className={'dataVisualizerColumnHeaderIcon'}>
|
||||
<EuiIcon type="document" size={'s'} />
|
||||
</EuiFlexItem>
|
||||
<EuiText size={'s'}>
|
||||
<b>{count}</b> ({docsPercent}%)
|
||||
return docsCount !== undefined ? (
|
||||
<>
|
||||
{showIcon ? <EuiIcon type="document" size={'m'} className={'columnHeader__icon'} /> : null}
|
||||
<EuiText size={'xs'}>
|
||||
{docsCount} ({docsPercent}%)
|
||||
</EuiText>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
</>
|
||||
) : null;
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
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 {
|
||||
MetricDistributionChart,
|
||||
|
@ -16,8 +16,8 @@ import {
|
|||
import { FieldVisConfig } from '../../types';
|
||||
import { kibanaFieldFormat, formatSingleValue } from '../../../utils';
|
||||
|
||||
const METRIC_DISTRIBUTION_CHART_WIDTH = 150;
|
||||
const METRIC_DISTRIBUTION_CHART_HEIGHT = 80;
|
||||
const METRIC_DISTRIBUTION_CHART_WIDTH = 100;
|
||||
const METRIC_DISTRIBUTION_CHART_HEIGHT = 10;
|
||||
|
||||
export interface NumberContentPreviewProps {
|
||||
config: FieldVisConfig;
|
||||
|
@ -59,8 +59,11 @@ export const IndexBasedNumberContentPreview: FC<NumberContentPreviewProps> = ({
|
|||
<div className={'dataGridChart__legend'} data-test-subj={`${dataTestSubj}-legend`}>
|
||||
{legendText && (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup direction={'row'} data-test-subj={`${dataTestSubj}-legend`}>
|
||||
<EuiFlexGroup
|
||||
direction={'row'}
|
||||
data-test-subj={`${dataTestSubj}-legend`}
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem className={'dataGridChart__legend'}>
|
||||
{kibanaFieldFormat(legendText.min, fieldFormat)}
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -122,8 +122,8 @@ describe('getLegendText()', () => {
|
|||
})}
|
||||
</>
|
||||
);
|
||||
expect(getByText('true')).toBeInTheDocument();
|
||||
expect(getByText('false')).toBeInTheDocument();
|
||||
expect(getByText('t')).toBeInTheDocument();
|
||||
expect(getByText('f')).toBeInTheDocument();
|
||||
});
|
||||
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(
|
||||
|
|
|
@ -94,11 +94,19 @@ export const getLegendText = (
|
|||
|
||||
if (chartData.type === 'boolean') {
|
||||
return (
|
||||
<table className="dataGridChart__legendBoolean">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
{chartData.data[0] !== undefined && <td>{chartData.data[0].key_as_string}</td>}
|
||||
{chartData.data[1] !== undefined && <td>{chartData.data[1].key_as_string}</td>}
|
||||
{chartData.data[0] !== undefined && (
|
||||
<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>
|
||||
</tbody>
|
||||
</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.
|
||||
// See TS Caveats for details: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-3.html#caveats
|
||||
if (isOrdinalChartData(chartData)) {
|
||||
data = chartData.data.map((d: OrdinalDataItem) => ({
|
||||
data = chartData.data.map((d: OrdinalDataItem, idx) => ({
|
||||
...d,
|
||||
x: idx,
|
||||
key_as_string: d.key_as_string ?? d.key,
|
||||
color: getColor(d),
|
||||
}));
|
||||
} else if (isNumericChartData(chartData)) {
|
||||
data = chartData.data.map((d: NumericDataItem) => ({
|
||||
data = chartData.data.map((d: NumericDataItem, idx) => ({
|
||||
...d,
|
||||
x: idx,
|
||||
key_as_string: d.key_as_string || d.key,
|
||||
color: getColor(d),
|
||||
}));
|
||||
|
|
|
@ -75,14 +75,17 @@ export const MetricDistributionChart: FC<Props> = ({
|
|||
return (
|
||||
<MetricDistributionChartTooltipHeader
|
||||
chartPoint={chartPoint}
|
||||
maxWidth={width / 2}
|
||||
maxWidth={width}
|
||||
fieldFormat={fieldFormat}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div data-test-subj="dataVisualizerFieldDataMetricDistributionChart">
|
||||
<div
|
||||
data-test-subj="dataVisualizerFieldDataMetricDistributionChart"
|
||||
className="dataGridChart__histogram"
|
||||
>
|
||||
<Chart size={{ width, height }}>
|
||||
<Settings theme={theme} tooltip={{ headerFormatter }} />
|
||||
<Axis
|
||||
|
|
|
@ -5,13 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import {
|
||||
CENTER_ALIGNMENT,
|
||||
EuiBasicTableColumn,
|
||||
EuiButtonIcon,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiInMemoryTable,
|
||||
EuiText,
|
||||
|
@ -19,13 +18,13 @@ import {
|
|||
HorizontalAlignment,
|
||||
LEFT_ALIGNMENT,
|
||||
RIGHT_ALIGNMENT,
|
||||
EuiResizeObserver,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiTableComputedColumnType } from '@elastic/eui/src/components/basic_table/table_types';
|
||||
import { throttle } from 'lodash';
|
||||
import { JOB_FIELD_TYPES, JobFieldType, DataVisualizerTableState } from '../../../../../common';
|
||||
import { FieldTypeIcon } from '../field_type_icon';
|
||||
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 { useTableSettings } from './use_table_settings';
|
||||
|
@ -37,6 +36,9 @@ import {
|
|||
} from './types/field_vis_config';
|
||||
import { FileBasedNumberContentPreview } from '../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';
|
||||
|
||||
|
@ -49,6 +51,9 @@ interface DataVisualizerTableProps<T> {
|
|||
updatePageState: (update: DataVisualizerTableState) => void;
|
||||
getItemIdToExpandedRowMap: (itemIds: string[], items: T[]) => ItemIdToExpandedRowMap;
|
||||
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>({
|
||||
|
@ -57,23 +62,52 @@ export const DataVisualizerTable = <T extends DataVisualizerTableItem>({
|
|||
updatePageState,
|
||||
getItemIdToExpandedRowMap,
|
||||
extendedColumns,
|
||||
showPreviewByDefault,
|
||||
onChange,
|
||||
}: DataVisualizerTableProps<T>) => {
|
||||
const [expandedRowItemIds, setExpandedRowItemIds] = useState<string[]>([]);
|
||||
const [expandAll, toggleExpandAll] = useState<boolean>(false);
|
||||
const [expandAll, setExpandAll] = useState<boolean>(false);
|
||||
|
||||
const { onTableChange, pagination, sorting } = useTableSettings<T>(
|
||||
items,
|
||||
pageState,
|
||||
updatePageState
|
||||
);
|
||||
const showDistributions: boolean =
|
||||
('showDistributions' in pageState && pageState.showDistributions) ?? true;
|
||||
const toggleShowDistribution = () => {
|
||||
updatePageState({
|
||||
...pageState,
|
||||
showDistributions: !showDistributions,
|
||||
});
|
||||
};
|
||||
const [showDistributions, setShowDistributions] = useState<boolean>(showPreviewByDefault ?? true);
|
||||
const [dimensions, setDimensions] = useState(calculateTableColumnsDimensions());
|
||||
const [tableWidth, setTableWidth] = useState<number>(1400);
|
||||
|
||||
const toggleExpandAll = useCallback(
|
||||
(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) {
|
||||
if (item.fieldName === undefined) return;
|
||||
|
@ -90,31 +124,32 @@ export const DataVisualizerTable = <T extends DataVisualizerTableItem>({
|
|||
|
||||
const columns = useMemo(() => {
|
||||
const expanderColumn: EuiTableComputedColumnType<DataVisualizerTableItem> = {
|
||||
name: (
|
||||
<EuiButtonIcon
|
||||
data-test-subj={`dataVisualizerToggleDetailsForAllRowsButton ${
|
||||
expandAll ? 'expanded' : 'collapsed'
|
||||
}`}
|
||||
onClick={() => toggleExpandAll(!expandAll)}
|
||||
aria-label={
|
||||
!expandAll
|
||||
? i18n.translate('xpack.dataVisualizer.dataGrid.expandDetailsForAllAriaLabel', {
|
||||
defaultMessage: 'Expand details for all fields',
|
||||
})
|
||||
: i18n.translate('xpack.dataVisualizer.dataGrid.collapseDetailsForAllAriaLabel', {
|
||||
defaultMessage: 'Collapse details for all fields',
|
||||
})
|
||||
}
|
||||
iconType={expandAll ? 'arrowUp' : 'arrowDown'}
|
||||
/>
|
||||
),
|
||||
name:
|
||||
dimensions.breakPoint !== 'xs' && dimensions.breakPoint !== 's' ? (
|
||||
<EuiButtonIcon
|
||||
data-test-subj={`dataVisualizerToggleDetailsForAllRowsButton ${
|
||||
expandAll ? 'expanded' : 'collapsed'
|
||||
}`}
|
||||
onClick={() => toggleExpandAll(!expandAll)}
|
||||
aria-label={
|
||||
!expandAll
|
||||
? i18n.translate('xpack.dataVisualizer.dataGrid.expandDetailsForAllAriaLabel', {
|
||||
defaultMessage: 'Expand details for all fields',
|
||||
})
|
||||
: i18n.translate('xpack.dataVisualizer.dataGrid.collapseDetailsForAllAriaLabel', {
|
||||
defaultMessage: 'Collapse details for all fields',
|
||||
})
|
||||
}
|
||||
iconType={expandAll ? 'arrowDown' : 'arrowRight'}
|
||||
/>
|
||||
) : null,
|
||||
align: RIGHT_ALIGNMENT,
|
||||
width: '40px',
|
||||
width: dimensions.expander,
|
||||
isExpander: true,
|
||||
render: (item: DataVisualizerTableItem) => {
|
||||
const displayName = item.displayName ?? item.fieldName;
|
||||
if (item.fieldName === undefined) return null;
|
||||
const direction = expandedRowItemIds.includes(item.fieldName) ? 'arrowUp' : 'arrowDown';
|
||||
const direction = expandedRowItemIds.includes(item.fieldName) ? 'arrowDown' : 'arrowRight';
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
data-test-subj={`dataVisualizerDetailsToggle-${item.fieldName}-${direction}`}
|
||||
|
@ -147,7 +182,7 @@ export const DataVisualizerTable = <T extends DataVisualizerTableItem>({
|
|||
render: (fieldType: JobFieldType) => {
|
||||
return <FieldTypeIcon type={fieldType} tooltipEnabled={true} needsAria={true} />;
|
||||
},
|
||||
width: '75px',
|
||||
width: dimensions.type,
|
||||
sortable: true,
|
||||
align: CENTER_ALIGNMENT as HorizontalAlignment,
|
||||
'data-test-subj': 'dataVisualizerTableColumnType',
|
||||
|
@ -163,8 +198,8 @@ export const DataVisualizerTable = <T extends DataVisualizerTableItem>({
|
|||
const displayName = item.displayName ?? item.fieldName;
|
||||
|
||||
return (
|
||||
<EuiText size="s">
|
||||
<b data-test-subj={`dataVisualizerDisplayName-${item.fieldName}`}>{displayName}</b>
|
||||
<EuiText size="xs" data-test-subj={`dataVisualizerDisplayName-${item.fieldName}`}>
|
||||
{displayName}
|
||||
</EuiText>
|
||||
);
|
||||
},
|
||||
|
@ -177,56 +212,65 @@ export const DataVisualizerTable = <T extends DataVisualizerTableItem>({
|
|||
defaultMessage: 'Documents (%)',
|
||||
}),
|
||||
render: (value: number | undefined, item: DataVisualizerTableItem) => (
|
||||
<DocumentStat config={item} />
|
||||
<DocumentStat config={item} showIcon={dimensions.showIcon} />
|
||||
),
|
||||
sortable: (item: DataVisualizerTableItem) => item?.stats?.count,
|
||||
align: LEFT_ALIGNMENT as HorizontalAlignment,
|
||||
'data-test-subj': 'dataVisualizerTableColumnDocumentsCount',
|
||||
width: dimensions.docCount,
|
||||
},
|
||||
{
|
||||
field: 'stats.cardinality',
|
||||
name: i18n.translate('xpack.dataVisualizer.dataGrid.distinctValuesColumnName', {
|
||||
defaultMessage: 'Distinct values',
|
||||
}),
|
||||
render: (cardinality?: number) => <DistinctValues cardinality={cardinality} />,
|
||||
render: (cardinality: number | undefined) => (
|
||||
<DistinctValues cardinality={cardinality} showIcon={dimensions.showIcon} />
|
||||
),
|
||||
|
||||
sortable: true,
|
||||
align: LEFT_ALIGNMENT as HorizontalAlignment,
|
||||
'data-test-subj': 'dataVisualizerTableColumnDistinctValues',
|
||||
width: dimensions.distinctValues,
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<EuiIcon type={'visBarVertical'} style={{ paddingRight: 4 }} />
|
||||
<div className={'columnHeader__title'}>
|
||||
{dimensions.showIcon ? (
|
||||
<EuiIcon type={'visBarVertical'} className={'columnHeader__icon'} />
|
||||
) : null}
|
||||
{i18n.translate('xpack.dataVisualizer.dataGrid.distributionsColumnName', {
|
||||
defaultMessage: 'Distributions',
|
||||
})}
|
||||
<EuiToolTip
|
||||
content={
|
||||
!showDistributions
|
||||
? 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={
|
||||
{
|
||||
<EuiToolTip
|
||||
content={
|
||||
!showDistributions
|
||||
? i18n.translate('xpack.dataVisualizer.dataGrid.showDistributionsAriaLabel', {
|
||||
? i18n.translate('xpack.dataVisualizer.dataGrid.showDistributionsTooltip', {
|
||||
defaultMessage: 'Show distributions',
|
||||
})
|
||||
: i18n.translate('xpack.dataVisualizer.dataGrid.hideDistributionsAriaLabel', {
|
||||
: i18n.translate('xpack.dataVisualizer.dataGrid.hideDistributionsTooltip', {
|
||||
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>
|
||||
),
|
||||
render: (item: DataVisualizerTableItem) => {
|
||||
|
@ -252,41 +296,49 @@ export const DataVisualizerTable = <T extends DataVisualizerTableItem>({
|
|||
|
||||
return null;
|
||||
},
|
||||
width: dimensions.distributions,
|
||||
align: LEFT_ALIGNMENT as HorizontalAlignment,
|
||||
'data-test-subj': 'dataVisualizerTableColumnDistribution',
|
||||
},
|
||||
];
|
||||
return extendedColumns ? [...baseColumns, ...extendedColumns] : baseColumns;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [expandAll, showDistributions, updatePageState, extendedColumns]);
|
||||
}, [
|
||||
expandAll,
|
||||
showDistributions,
|
||||
updatePageState,
|
||||
extendedColumns,
|
||||
dimensions.breakPoint,
|
||||
toggleExpandAll,
|
||||
]);
|
||||
|
||||
const itemIdToExpandedRowMap = useMemo(() => {
|
||||
let itemIds = expandedRowItemIds;
|
||||
if (expandAll) {
|
||||
itemIds = items.map((i) => i[FIELD_NAME]).filter((f) => f !== undefined) as string[];
|
||||
}
|
||||
const itemIds = expandedRowItemIds;
|
||||
return getItemIdToExpandedRowMap(itemIds, items);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [expandAll, items, expandedRowItemIds]);
|
||||
}, [items, expandedRowItemIds, getItemIdToExpandedRowMap]);
|
||||
|
||||
return (
|
||||
<EuiFlexItem data-test-subj="dataVisualizerTableContainer">
|
||||
<EuiInMemoryTable<T>
|
||||
className={'dataVisualizer'}
|
||||
items={items}
|
||||
itemId={FIELD_NAME}
|
||||
columns={columns}
|
||||
pagination={pagination}
|
||||
sorting={sorting}
|
||||
isExpandable={true}
|
||||
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
|
||||
isSelectable={false}
|
||||
onTableChange={onTableChange}
|
||||
data-test-subj={'dataVisualizerTable'}
|
||||
rowProps={(item) => ({
|
||||
'data-test-subj': `dataVisualizerRow row-${item.fieldName}`,
|
||||
})}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiResizeObserver onResize={resizeHandler}>
|
||||
{(resizeRef) => (
|
||||
<div data-test-subj="dataVisualizerTableContainer" ref={resizeRef}>
|
||||
<EuiInMemoryTable<T>
|
||||
className={'dvTable'}
|
||||
items={items}
|
||||
itemId={FIELD_NAME}
|
||||
columns={columns}
|
||||
pagination={pagination}
|
||||
sorting={sorting}
|
||||
isExpandable={true}
|
||||
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
|
||||
isSelectable={false}
|
||||
onTableChange={onTableChange}
|
||||
data-test-subj={'dataVisualizerTable'}
|
||||
rowProps={(item) => ({
|
||||
'data-test-subj': `dataVisualizerRow row-${item.fieldName}`,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</EuiResizeObserver>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
*/
|
||||
|
||||
import type { FieldVisConfig, FileBasedFieldVisConfig } from './field_vis_config';
|
||||
import { IndexPatternField } from '../../../../../../../../../src/plugins/data/common';
|
||||
|
||||
export interface FieldDataRowProps {
|
||||
config: FieldVisConfig | FileBasedFieldVisConfig;
|
||||
onAddFilter?: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import { useCallback, useMemo } from 'react';
|
|||
|
||||
import { DataVisualizerTableState } from '../../../../../common';
|
||||
|
||||
const PAGE_SIZE_OPTIONS = [10, 25, 50];
|
||||
const PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
|
||||
|
||||
interface UseTableSettingsReturnValue<T> {
|
||||
onTableChange: EuiBasicTableProps<T>['onChange'];
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getBreakpoint } from '@elastic/eui';
|
||||
import { FileBasedFieldVisConfig } from './types';
|
||||
|
||||
export const getTFPercentage = (config: FileBasedFieldVisConfig) => {
|
||||
|
@ -36,3 +37,45 @@ export const getTFPercentage = (config: FileBasedFieldVisConfig) => {
|
|||
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 {
|
||||
margin-right: $euiSizeM;
|
||||
&.topValuesValueLabelContainer--small {
|
||||
width:70px;
|
||||
}
|
||||
|
||||
&.topValuesValueLabelContainer--large {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.topValuesPercentLabelContainer {
|
||||
margin-left: $euiSizeM;
|
||||
width:70px;
|
||||
}
|
||||
|
|
|
@ -12,21 +12,25 @@ import {
|
|||
EuiProgress,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiToolTip,
|
||||
EuiButtonIcon,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { roundToDecimalPlace, kibanaFieldFormat } from '../utils';
|
||||
import { ExpandedRowFieldHeader } from '../stats_table/components/expanded_row_field_header';
|
||||
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 {
|
||||
stats: FieldVisStats | undefined;
|
||||
fieldFormat?: any;
|
||||
barColor?: 'primary' | 'secondary' | 'danger' | 'subdued' | 'accent';
|
||||
compressed?: boolean;
|
||||
onAddFilter?: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
|
||||
}
|
||||
|
||||
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;
|
||||
const { topValues, topValuesSampleSize, topValuesSamplerShardSize, count, isTopValuesSampled } =
|
||||
stats;
|
||||
const {
|
||||
topValues,
|
||||
topValuesSampleSize,
|
||||
topValuesSamplerShardSize,
|
||||
count,
|
||||
isTopValuesSampled,
|
||||
fieldName,
|
||||
} = stats;
|
||||
|
||||
const progressBarMax = isTopValuesSampled === true ? topValuesSampleSize : count;
|
||||
return (
|
||||
<EuiFlexItem data-test-subj={'dataVisualizerFieldDataTopValues'}>
|
||||
<ExpandedRowPanel
|
||||
dataTestSubj={'dataVisualizerFieldDataTopValues'}
|
||||
className={classNames('dvPanel__wrapper', compressed ? 'dvPanel--compressed' : undefined)}
|
||||
>
|
||||
<ExpandedRowFieldHeader>
|
||||
<FormattedMessage
|
||||
id="xpack.dataVisualizer.dataGrid.field.topValuesLabel"
|
||||
|
@ -54,49 +68,90 @@ export const TopValues: FC<Props> = ({ stats, fieldFormat, barColor, compressed
|
|||
|
||||
<div
|
||||
data-test-subj="dataVisualizerFieldDataTopValuesContent"
|
||||
className={'fieldDataTopValuesContainer'}
|
||||
className={classNames('fieldDataTopValuesContainer', 'dvTopValues__wrapper')}
|
||||
>
|
||||
{Array.isArray(topValues) &&
|
||||
topValues.map((value) => (
|
||||
<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">
|
||||
<EuiProgress
|
||||
value={value.doc_count}
|
||||
max={progressBarMax}
|
||||
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>
|
||||
{progressBarMax !== undefined && (
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
className={classNames('eui-textTruncate', 'topValuesPercentLabelContainer')}
|
||||
>
|
||||
<EuiText size="xs" textAlign="left" color="subdued">
|
||||
{getPercentLabel(value.doc_count, progressBarMax)}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{fieldName !== undefined && value.key !== undefined && onAddFilter !== undefined ? (
|
||||
<>
|
||||
<EuiButtonIcon
|
||||
iconSize="s"
|
||||
iconType="plusInCircle"
|
||||
onClick={() =>
|
||||
onAddFilter(
|
||||
fieldName,
|
||||
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>
|
||||
))}
|
||||
{isTopValuesSampled === true && (
|
||||
<Fragment>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiText size="xs" textAlign={'left'}>
|
||||
<EuiText size="xs" textAlign={'center'}>
|
||||
<FormattedMessage
|
||||
id="xpack.dataVisualizer.dataGrid.field.topValues.calculatedFromSampleDescription"
|
||||
defaultMessage="Calculated from sample of {topValuesSamplerShardSize} documents per shard"
|
||||
|
@ -108,6 +163,6 @@ export const TopValues: FC<Props> = ({ stats, fieldFormat, barColor, compressed
|
|||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
</ExpandedRowPanel>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -23,6 +23,9 @@ export const jobTypeAriaLabels = {
|
|||
geoPointParam: 'geo point',
|
||||
},
|
||||
}),
|
||||
GEO_SHAPE: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.geoShapeTypeAriaLabel', {
|
||||
defaultMessage: 'geo shape type',
|
||||
}),
|
||||
IP: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.ipTypeAriaLabel', {
|
||||
defaultMessage: 'ip type',
|
||||
}),
|
||||
|
@ -32,6 +35,9 @@ export const jobTypeAriaLabels = {
|
|||
NUMBER: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.numberTypeAriaLabel', {
|
||||
defaultMessage: 'number type',
|
||||
}),
|
||||
HISTOGRAM: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.histogramTypeAriaLabel', {
|
||||
defaultMessage: 'histogram type',
|
||||
}),
|
||||
TEXT: i18n.translate('xpack.dataVisualizer.fieldTypeIcon.textTypeAriaLabel', {
|
||||
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) => {
|
||||
const requestedFieldType = Object.keys(JOB_FIELD_TYPES).find(
|
||||
(k) => JOB_FIELD_TYPES[k as keyof typeof JOB_FIELD_TYPES] === type
|
||||
|
|
|
@ -40,6 +40,7 @@ export const ActionsPanel: FC<Props> = ({
|
|||
|
||||
const {
|
||||
services: {
|
||||
data,
|
||||
application: { capabilities },
|
||||
share: {
|
||||
urlGenerators: { getUrlGenerator },
|
||||
|
@ -60,6 +61,9 @@ export const ActionsPanel: FC<Props> = ({
|
|||
const state: DiscoverUrlGeneratorState = {
|
||||
indexPatternId,
|
||||
};
|
||||
|
||||
state.filters = data.query.filterManager.getFilters() ?? [];
|
||||
|
||||
if (searchString && searchQueryLanguage !== undefined) {
|
||||
state.query = { query: searchString, language: searchQueryLanguage };
|
||||
}
|
||||
|
@ -113,6 +117,7 @@ export const ActionsPanel: FC<Props> = ({
|
|||
capabilities,
|
||||
getUrlGenerator,
|
||||
additionalLinks,
|
||||
data.query,
|
||||
]);
|
||||
|
||||
// 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 { Required } from 'utility-types';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import {
|
||||
IndexPatternField,
|
||||
KBN_FIELD_TYPES,
|
||||
UI_SETTINGS,
|
||||
Query,
|
||||
IndexPattern,
|
||||
generateFilters,
|
||||
} from '../../../../../../../../src/plugins/data/public';
|
||||
import { FullTimeRangeSelector } from '../full_time_range_selector';
|
||||
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 { HelpMenu } from '../../../common/components/help_menu';
|
||||
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 { ResultLink } from '../../../common/components/results_links';
|
||||
import { extractErrorProperties } from '../../utils/error_utils';
|
||||
import { IndexPatternField, IndexPattern } from '../../../../../../../../src/plugins/data/common';
|
||||
import './_index.scss';
|
||||
|
||||
interface DataVisualizerPageState {
|
||||
overallStats: OverallStats;
|
||||
|
@ -85,7 +87,7 @@ const defaultSearchQuery = {
|
|||
match_all: {},
|
||||
};
|
||||
|
||||
function getDefaultPageState(): DataVisualizerPageState {
|
||||
export function getDefaultPageState(): DataVisualizerPageState {
|
||||
return {
|
||||
overallStats: {
|
||||
totalCount: 0,
|
||||
|
@ -103,22 +105,25 @@ function getDefaultPageState(): DataVisualizerPageState {
|
|||
documentCountStats: undefined,
|
||||
};
|
||||
}
|
||||
export const getDefaultDataVisualizerListState =
|
||||
(): Required<DataVisualizerIndexBasedAppState> => ({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
sortField: 'fieldName',
|
||||
sortDirection: 'asc',
|
||||
visibleFieldTypes: [],
|
||||
visibleFieldNames: [],
|
||||
samplerShardSize: 5000,
|
||||
searchString: '',
|
||||
searchQuery: defaultSearchQuery,
|
||||
searchQueryLanguage: SEARCH_QUERY_LANGUAGE.KUERY,
|
||||
showDistributions: true,
|
||||
showAllFields: false,
|
||||
showEmptyFields: false,
|
||||
});
|
||||
export const getDefaultDataVisualizerListState = (
|
||||
overrides?: Partial<DataVisualizerIndexBasedAppState>
|
||||
): Required<DataVisualizerIndexBasedAppState> => ({
|
||||
pageIndex: 0,
|
||||
pageSize: 25,
|
||||
sortField: 'fieldName',
|
||||
sortDirection: 'asc',
|
||||
visibleFieldTypes: [],
|
||||
visibleFieldNames: [],
|
||||
samplerShardSize: 5000,
|
||||
searchString: '',
|
||||
searchQuery: defaultSearchQuery,
|
||||
searchQueryLanguage: SEARCH_QUERY_LANGUAGE.KUERY,
|
||||
filters: [],
|
||||
showDistributions: true,
|
||||
showAllFields: false,
|
||||
showEmptyFields: false,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
export interface IndexDataVisualizerViewProps {
|
||||
currentIndexPattern: IndexPattern;
|
||||
|
@ -129,7 +134,7 @@ const restorableDefaults = getDefaultDataVisualizerListState();
|
|||
|
||||
export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVisualizerProps) => {
|
||||
const { services } = useDataVisualizerKibana();
|
||||
const { docLinks, notifications, uiSettings } = services;
|
||||
const { docLinks, notifications, uiSettings, data } = services;
|
||||
const { toasts } = notifications;
|
||||
|
||||
const [dataVisualizerListState, setDataVisualizerListState] = usePageUrlState(
|
||||
|
@ -150,6 +155,15 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
|
|||
}
|
||||
}, [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(() => {
|
||||
return new TimeBuckets({
|
||||
[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 { searchQueryLanguage, searchString, searchQuery } = useMemo(() => {
|
||||
const searchData = extractSearchData(
|
||||
currentSavedSearch,
|
||||
currentIndexPattern,
|
||||
uiSettings.get(UI_SETTINGS.QUERY_STRING_OPTIONS)
|
||||
);
|
||||
const searchData = getEsQueryFromSavedSearch({
|
||||
indexPattern: currentIndexPattern,
|
||||
uiSettings,
|
||||
savedSearch: currentSavedSearch,
|
||||
filterManager: data.query.filterManager,
|
||||
});
|
||||
|
||||
if (searchData === undefined || dataVisualizerListState.searchString !== '') {
|
||||
if (dataVisualizerListState.filters) {
|
||||
data.query.filterManager.setFilters(dataVisualizerListState.filters);
|
||||
}
|
||||
return {
|
||||
searchQuery: dataVisualizerListState.searchQuery,
|
||||
searchString: dataVisualizerListState.searchString,
|
||||
|
@ -247,26 +265,31 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
|
|||
};
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentSavedSearch, currentIndexPattern, dataVisualizerListState]);
|
||||
}, [currentSavedSearch, currentIndexPattern, dataVisualizerListState, data.query]);
|
||||
|
||||
const setSearchParams = (searchParams: {
|
||||
searchQuery: Query['query'];
|
||||
searchString: Query['query'];
|
||||
queryLanguage: SearchQueryLanguage;
|
||||
}) => {
|
||||
// When the user loads saved search and then clear or modify the query
|
||||
// we should remove the saved search and replace it with the index pattern id
|
||||
if (currentSavedSearch !== null) {
|
||||
setCurrentSavedSearch(null);
|
||||
}
|
||||
const setSearchParams = useCallback(
|
||||
(searchParams: {
|
||||
searchQuery: Query['query'];
|
||||
searchString: Query['query'];
|
||||
queryLanguage: SearchQueryLanguage;
|
||||
filters: Filter[];
|
||||
}) => {
|
||||
// When the user loads saved search and then clear or modify the query
|
||||
// we should remove the saved search and replace it with the index pattern id
|
||||
if (currentSavedSearch !== null) {
|
||||
setCurrentSavedSearch(null);
|
||||
}
|
||||
|
||||
setDataVisualizerListState({
|
||||
...dataVisualizerListState,
|
||||
searchQuery: searchParams.searchQuery,
|
||||
searchString: searchParams.searchString,
|
||||
searchQueryLanguage: searchParams.queryLanguage,
|
||||
});
|
||||
};
|
||||
setDataVisualizerListState({
|
||||
...dataVisualizerListState,
|
||||
searchQuery: searchParams.searchQuery,
|
||||
searchString: searchParams.searchString,
|
||||
searchQueryLanguage: searchParams.queryLanguage,
|
||||
filters: searchParams.filters,
|
||||
});
|
||||
},
|
||||
[currentSavedSearch, dataVisualizerListState, setDataVisualizerListState]
|
||||
);
|
||||
|
||||
const samplerShardSize =
|
||||
dataVisualizerListState.samplerShardSize ?? restorableDefaults.samplerShardSize;
|
||||
|
@ -305,6 +328,52 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
|
|||
const [nonMetricConfigs, setNonMetricConfigs] = useState(defaults.nonMetricConfigs);
|
||||
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(() => {
|
||||
const timeUpdateSubscription = merge(
|
||||
timefilter.getTimeUpdate$(),
|
||||
|
@ -666,11 +735,11 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
|
|||
const fieldData = nonMetricFieldData.find((f) => f.fieldName === field.spec.name);
|
||||
|
||||
const nonMetricConfig = {
|
||||
...fieldData,
|
||||
...(fieldData ? fieldData : {}),
|
||||
fieldFormat: currentIndexPattern.getFormatterForField(field),
|
||||
aggregatable: field.aggregatable,
|
||||
scripted: field.scripted,
|
||||
loading: fieldData.existsInDocs,
|
||||
loading: fieldData?.existsInDocs,
|
||||
deletable: field.runtimeField !== undefined,
|
||||
};
|
||||
|
||||
|
@ -751,13 +820,14 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
|
|||
item={item}
|
||||
indexPattern={currentIndexPattern}
|
||||
combinedQuery={{ searchQueryLanguage, searchString }}
|
||||
onAddFilter={onAddFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return m;
|
||||
}, {} as ItemIdToExpandedRowMap);
|
||||
},
|
||||
[currentIndexPattern, searchQueryLanguage, searchString]
|
||||
[currentIndexPattern, searchQueryLanguage, searchString, onAddFilter]
|
||||
);
|
||||
|
||||
// Some actions open up fly-out or popup
|
||||
|
@ -809,17 +879,10 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
|
|||
<EuiPageBody>
|
||||
<EuiFlexGroup gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<EuiPageContentHeader>
|
||||
<EuiPageContentHeader className="dataVisualizerPageHeader">
|
||||
<EuiPageContentHeaderSection>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<EuiTitle size="l">
|
||||
<div className="dataViewTitleHeader">
|
||||
<EuiTitle>
|
||||
<h1>{currentIndexPattern.title}</h1>
|
||||
</EuiTitle>
|
||||
<DataVisualizerIndexPatternManagement
|
||||
|
@ -829,23 +892,26 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
|
|||
</div>
|
||||
</EuiPageContentHeaderSection>
|
||||
|
||||
<EuiPageContentHeaderSection data-test-subj="dataVisualizerTimeRangeSelectorSection">
|
||||
<EuiFlexGroup alignItems="center" justifyContent="flexEnd" gutterSize="s">
|
||||
{currentIndexPattern.timeFieldName !== undefined && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<FullTimeRangeSelector
|
||||
indexPattern={currentIndexPattern}
|
||||
query={undefined}
|
||||
disabled={false}
|
||||
timefilter={timefilter}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
justifyContent="flexEnd"
|
||||
gutterSize="s"
|
||||
data-test-subj="dataVisualizerTimeRangeSelectorSection"
|
||||
>
|
||||
{currentIndexPattern.timeFieldName !== undefined && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<DatePickerWrapper />
|
||||
<FullTimeRangeSelector
|
||||
indexPattern={currentIndexPattern}
|
||||
query={undefined}
|
||||
disabled={false}
|
||||
timefilter={timefilter}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPageContentHeaderSection>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<DatePickerWrapper />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPageContentHeader>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
@ -862,8 +928,6 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiSpacer size={'m'} />
|
||||
|
||||
<SearchPanel
|
||||
indexPattern={currentIndexPattern}
|
||||
searchString={searchString}
|
||||
|
@ -879,8 +943,9 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
|
|||
visibleFieldNames={visibleFieldNames}
|
||||
setVisibleFieldNames={setVisibleFieldNames}
|
||||
showEmptyFields={showEmptyFields}
|
||||
onAddFilter={onAddFilter}
|
||||
/>
|
||||
<EuiSpacer size={'l'} />
|
||||
<EuiSpacer size={'m'} />
|
||||
<FieldCountPanel
|
||||
showEmptyFields={showEmptyFields}
|
||||
toggleShowEmptyFields={toggleShowEmptyFields}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
@import 'search_panel';
|
|
@ -8,32 +8,28 @@
|
|||
import React, { FC, useMemo } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
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 { 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[];
|
||||
setVisibleFieldTypes(q: string[]): void;
|
||||
visibleFieldTypes: string[];
|
||||
}> = ({ indexedFieldTypes, setVisibleFieldTypes, visibleFieldTypes }) => {
|
||||
const options: Option[] = useMemo(() => {
|
||||
return indexedFieldTypes.map((indexedFieldName) => {
|
||||
const item = JOB_FIELD_TYPES_OPTIONS[indexedFieldName];
|
||||
const label = jobTypeLabels[indexedFieldName] ?? '';
|
||||
|
||||
return {
|
||||
value: indexedFieldName,
|
||||
name: (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={true}> {item.name}</EuiFlexItem>
|
||||
<EuiFlexItem grow={true}> {label}</EuiFlexItem>
|
||||
{indexedFieldName && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<FieldTypeIcon
|
||||
type={indexedFieldName}
|
||||
fieldName={item.name}
|
||||
tooltipEnabled={false}
|
||||
needsAria={true}
|
||||
/>
|
||||
<FieldTypeIcon type={indexedFieldName} tooltipEnabled={false} needsAria={true} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</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 { EuiCode, EuiFlexItem, EuiFlexGroup, EuiInputPopover } from '@elastic/eui';
|
||||
import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Query, fromKueryExpression, luceneStringToDsl, toElasticsearchQuery } from '@kbn/es-query';
|
||||
import { QueryStringInput } from '../../../../../../../../src/plugins/data/public';
|
||||
import { Query, Filter } from '@kbn/es-query';
|
||||
import { ShardSizeFilter } from './shard_size_select';
|
||||
import { DataVisualizerFieldNamesFilter } from './field_name_filter';
|
||||
import { DatavisualizerFieldTypeFilter } from './field_type_filter';
|
||||
import { IndexPattern } from '../../../../../../../../src/plugins/data/common';
|
||||
import { JobFieldType } from '../../../../../common/types';
|
||||
import { DataVisualizerFieldTypeFilter } from './field_type_filter';
|
||||
import {
|
||||
ErrorMessage,
|
||||
SEARCH_QUERY_LANGUAGE,
|
||||
SearchQueryLanguage,
|
||||
} from '../../types/combined_query';
|
||||
|
||||
IndexPattern,
|
||||
IndexPatternField,
|
||||
TimeRange,
|
||||
} 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 {
|
||||
indexPattern: IndexPattern;
|
||||
searchString: Query['query'];
|
||||
|
@ -38,12 +39,15 @@ interface Props {
|
|||
searchQuery,
|
||||
searchString,
|
||||
queryLanguage,
|
||||
filters,
|
||||
}: {
|
||||
searchQuery: Query['query'];
|
||||
searchString: Query['query'];
|
||||
queryLanguage: SearchQueryLanguage;
|
||||
filters: Filter[];
|
||||
}): void;
|
||||
showEmptyFields: boolean;
|
||||
onAddFilter?: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
|
||||
}
|
||||
|
||||
export const SearchPanel: FC<Props> = ({
|
||||
|
@ -61,98 +65,109 @@ export const SearchPanel: FC<Props> = ({
|
|||
setSearchParams,
|
||||
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.
|
||||
const [searchInput, setSearchInput] = useState<Query>({
|
||||
query: searchString || '',
|
||||
language: searchQueryLanguage,
|
||||
});
|
||||
const [errorMessage, setErrorMessage] = useState<ErrorMessage | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
setSearchInput({
|
||||
query: searchString || '',
|
||||
language: searchQueryLanguage,
|
||||
});
|
||||
}, [searchQueryLanguage, searchString]);
|
||||
}, [searchQueryLanguage, searchString, queryManager.filterManager]);
|
||||
|
||||
const searchHandler = (query: Query) => {
|
||||
let filterQuery;
|
||||
const searchHandler = ({ query, filters }: { query?: Query; filters?: Filter[] }) => {
|
||||
const mergedQuery = query ?? searchInput;
|
||||
const mergedFilters = filters ?? queryManager.filterManager.getFilters();
|
||||
try {
|
||||
if (query.language === SEARCH_QUERY_LANGUAGE.KUERY) {
|
||||
filterQuery = toElasticsearchQuery(fromKueryExpression(query.query), indexPattern);
|
||||
} else if (query.language === SEARCH_QUERY_LANGUAGE.LUCENE) {
|
||||
filterQuery = luceneStringToDsl(query.query);
|
||||
} else {
|
||||
filterQuery = {};
|
||||
if (mergedFilters) {
|
||||
queryManager.filterManager.setFilters(mergedFilters);
|
||||
}
|
||||
|
||||
const combinedQuery = createMergedEsQuery(
|
||||
mergedQuery,
|
||||
queryManager.filterManager.getFilters() ?? [],
|
||||
indexPattern,
|
||||
uiSettings
|
||||
);
|
||||
|
||||
setSearchParams({
|
||||
searchQuery: filterQuery,
|
||||
searchString: query.query,
|
||||
queryLanguage: query.language as SearchQueryLanguage,
|
||||
searchQuery: combinedQuery,
|
||||
searchString: mergedQuery.query,
|
||||
queryLanguage: mergedQuery.language as SearchQueryLanguage,
|
||||
filters: mergedFilters,
|
||||
});
|
||||
} catch (e) {
|
||||
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 (
|
||||
<EuiFlexGroup gutterSize="m" alignItems="center" data-test-subj="dataVisualizerSearchPanel">
|
||||
<EuiFlexItem>
|
||||
<EuiInputPopover
|
||||
style={{ maxWidth: '100%' }}
|
||||
closePopover={() => setErrorMessage(undefined)}
|
||||
input={
|
||||
<QueryStringInput
|
||||
bubbleSubmitEvent={false}
|
||||
query={searchInput}
|
||||
indexPatterns={[indexPattern]}
|
||||
onChange={searchChangeHandler}
|
||||
onSubmit={searchHandler}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.dataVisualizer.searchPanel.queryBarPlaceholderText',
|
||||
{
|
||||
defaultMessage: 'Search… (e.g. status:200 AND extension:"PHP")',
|
||||
}
|
||||
)}
|
||||
disableAutoFocus={true}
|
||||
dataTestSubj="dataVisualizerQueryInput"
|
||||
languageSwitcherPopoverAnchorPosition="rightDown"
|
||||
/>
|
||||
<EuiFlexGroup
|
||||
gutterSize="s"
|
||||
alignItems="flexStart"
|
||||
data-test-subj="dataVisualizerSearchPanel"
|
||||
className={'dvSearchPanel__container'}
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem grow={9} className={'dvSearchBar'}>
|
||||
<SearchBar
|
||||
dataTestSubj="dataVisualizerQueryInput"
|
||||
appName={'dataVisualizer'}
|
||||
showFilterBar={true}
|
||||
showDatePicker={false}
|
||||
showQueryInput={true}
|
||||
query={searchInput}
|
||||
onQuerySubmit={(params: { dateRange: TimeRange; query?: Query | undefined }) =>
|
||||
searchHandler({ query: params.query })
|
||||
}
|
||||
isOpen={errorMessage?.query === searchInput.query && errorMessage?.message !== ''}
|
||||
>
|
||||
<EuiCode>
|
||||
{i18n.translate(
|
||||
'xpack.dataVisualizer.searchPanel.invalidKuerySyntaxErrorMessageQueryBar',
|
||||
{
|
||||
defaultMessage: 'Invalid query',
|
||||
}
|
||||
)}
|
||||
{': '}
|
||||
{errorMessage?.message.split('\n')[0]}
|
||||
</EuiCode>
|
||||
</EuiInputPopover>
|
||||
// @ts-expect-error onFiltersUpdated is a valid prop on SearchBar
|
||||
onFiltersUpdated={(filters: Filter[]) => searchHandler({ filters })}
|
||||
indexPatterns={[indexPattern]}
|
||||
placeholder={i18n.translate('xpack.dataVisualizer.searchPanel.queryBarPlaceholderText', {
|
||||
defaultMessage: 'Search… (e.g. status:200 AND extension:"PHP")',
|
||||
})}
|
||||
displayStyle={'inPage'}
|
||||
isClearable={true}
|
||||
customSubmitButton={<div />}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexItem grow={2} className={'dvSearchPanel__controls'}>
|
||||
<ShardSizeFilter
|
||||
samplerShardSize={samplerShardSize}
|
||||
setSamplerShardSize={setSamplerShardSize}
|
||||
/>
|
||||
|
||||
<DataVisualizerFieldNamesFilter
|
||||
overallStats={overallStats}
|
||||
setVisibleFieldNames={setVisibleFieldNames}
|
||||
visibleFieldNames={visibleFieldNames}
|
||||
showEmptyFields={showEmptyFields}
|
||||
/>
|
||||
<DataVisualizerFieldTypeFilter
|
||||
indexedFieldTypes={indexedFieldTypes}
|
||||
setVisibleFieldTypes={setVisibleFieldTypes}
|
||||
visibleFieldTypes={visibleFieldTypes}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<DataVisualizerFieldNamesFilter
|
||||
overallStats={overallStats}
|
||||
setVisibleFieldNames={setVisibleFieldNames}
|
||||
visibleFieldNames={visibleFieldNames}
|
||||
showEmptyFields={showEmptyFields}
|
||||
/>
|
||||
<DatavisualizerFieldTypeFilter
|
||||
indexedFieldTypes={indexedFieldTypes}
|
||||
setVisibleFieldTypes={setVisibleFieldTypes}
|
||||
visibleFieldTypes={visibleFieldTypes}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -49,6 +49,7 @@ export const DataVisualizerUrlStateContextProvider: FC<DataVisualizerUrlStateCon
|
|||
},
|
||||
} = useDataVisualizerKibana();
|
||||
const history = useHistory();
|
||||
const { search: searchString } = useLocation();
|
||||
|
||||
const [currentIndexPattern, setCurrentIndexPattern] = useState<IndexPattern | undefined>(
|
||||
undefined
|
||||
|
@ -56,7 +57,6 @@ export const DataVisualizerUrlStateContextProvider: FC<DataVisualizerUrlStateCon
|
|||
const [currentSavedSearch, setCurrentSavedSearch] = useState<SimpleSavedObject<unknown> | null>(
|
||||
null
|
||||
);
|
||||
const { search: searchString } = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
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.
|
||||
*/
|
||||
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import { Query } from '../../../../../../../src/plugins/data/common/query';
|
||||
import { SearchQueryLanguage } from './combined_query';
|
||||
|
||||
|
@ -25,4 +26,5 @@ export interface DataVisualizerIndexBasedAppState extends Omit<ListingPageUrlSta
|
|||
showDistributions?: boolean;
|
||||
showAllFields?: 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 { IUiSettingsClient } from 'kibana/public';
|
||||
import {
|
||||
buildEsQuery,
|
||||
buildQueryFromFilters,
|
||||
decorateQuery,
|
||||
fromKueryExpression,
|
||||
luceneStringToDsl,
|
||||
toElasticsearchQuery,
|
||||
buildQueryFromFilters,
|
||||
buildEsQuery,
|
||||
Query,
|
||||
Filter,
|
||||
} from '@kbn/es-query';
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import { SavedSearchSavedObject } from '../../../../common/types';
|
||||
import { isSavedSearchSavedObject, SavedSearchSavedObject } from '../../../../common/types';
|
||||
import { IndexPattern } from '../../../../../../../src/plugins/data/common';
|
||||
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 };
|
||||
return JSON.parse(search.searchSourceJSON) as {
|
||||
query: Query;
|
||||
filter: any[];
|
||||
};
|
||||
/**
|
||||
* Parse the stringified searchSourceJSON
|
||||
* from a saved search or saved search object
|
||||
*/
|
||||
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(
|
||||
savedSearch: SavedSearchSavedObject | null,
|
||||
currentIndexPattern: IndexPattern,
|
||||
queryStringOptions: Record<string, any> | string
|
||||
export function createMergedEsQuery(
|
||||
query?: Query,
|
||||
filters?: Filter[],
|
||||
indexPattern?: IndexPattern,
|
||||
uiSettings?: IUiSettingsClient
|
||||
) {
|
||||
if (!savedSearch) {
|
||||
return undefined;
|
||||
let combinedQuery: any = getDefaultQuery();
|
||||
|
||||
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);
|
||||
const queryLanguage = extractedQuery.language as SearchQueryLanguage;
|
||||
const qryString = extractedQuery.query;
|
||||
let qry;
|
||||
if (queryLanguage === SEARCH_QUERY_LANGUAGE.KUERY) {
|
||||
const ast = fromKueryExpression(qryString);
|
||||
qry = toElasticsearchQuery(ast, currentIndexPattern);
|
||||
} else {
|
||||
qry = luceneStringToDsl(qryString);
|
||||
decorateQuery(qry, queryStringOptions);
|
||||
// If saved search available, merge saved search with latest user query or filters differ from extracted saved search data
|
||||
if (savedSearchData) {
|
||||
const currentQuery = userQuery ?? savedSearchData?.query;
|
||||
const currentFilters = userFilters ?? savedSearchData?.filter;
|
||||
|
||||
if (filterManager) filterManager.setFilters(currentFilters);
|
||||
|
||||
const combinedQuery = createMergedEsQuery(
|
||||
currentQuery,
|
||||
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 = {
|
||||
|
@ -69,64 +169,6 @@ const DEFAULT_QUERY = {
|
|||
},
|
||||
};
|
||||
|
||||
export function getDefaultDatafeedQuery() {
|
||||
export function getDefaultQuery() {
|
||||
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
|
||||
>
|
||||
{
|
||||
public setup(core: CoreSetup, plugins: DataVisualizerSetupDependencies) {
|
||||
public setup(
|
||||
core: CoreSetup<DataVisualizerStartDependencies, DataVisualizerPluginStart>,
|
||||
plugins: DataVisualizerSetupDependencies
|
||||
) {
|
||||
if (plugins.home) {
|
||||
registerHomeAddData(plugins.home);
|
||||
registerHomeFeatureCatalogue(plugins.home);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue