mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[ML] Add Index data visualizer grid embeddable as extra view within Discover (#107184)
* [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] Initial embed * [ML] Initial embed props * Add embeddable 1 * Add visible fields * Embeddable 2 * 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 * Fix missing code after conflicts * Remove dv locator and flyout * Make types happy * Fix types * Rename toggle option * Resolve conflicts * [ML] Reduce size of chart * [ML] Unbold name, switch icons of show distributions * [ML] Make size consistent * [ML] Make page size 25 * [ML] Switch to arrow right and down * [ML] Make legend font smaller * [ML] Add user setting * [ML] Add show preview by default setting * [ML] Match icon * Add panels around the subcontent * Add preference for aggregated vs doc * Fix types * Fix types, add constants for adv settings * Change to data view type * Temp fix for Kibana/EUI table overflow issue * Modify line height so text is not cut off, modify widths for varying screen sizes * Different width padders for different screens * Fix CI * Merge latest, move button to the right * Fix width for bar charts previews * Fix toggle buttons, fix maps * Delete unused file * Fix boolean styling * Change to enum, discover mode * Hide field stats * Hide field stats * Persist show mini preview/distribution settings * Remove window size, use size observer instead * Default to document view * Remove bold, switch icon * Set fixed width for top values, reduce font size in table * Fix custom url tests * Update width styling for panels * Fix missing flag for Discover sidebar, jest tests * Fix max width * Workaround for sorting * Fix import * Fix styling * Make height uniform, center alignment, fix map and keyword map not same size Move styling * Revert "Make height uniform, center alignment, fix map and keyword map not same size" This reverts commit8fc42e2f
* Revert "Make height uniform, center alignment, fix map and keyword map not same size" This reverts commit8fc42e2f
* Uniform height, left aligned, flex grid * Switch top values to have labels * Center content * Replace fixed widths with percentage * Fix table missing field types * Add dashboard embeddable and filter support * Fix file data viz styling and tests, lean up imports, remove hard coded pixels * Add search panel/kql filter bar * Temporarily fix scrolling * New kql filters for data visualizer * Set map height so it will fit the sampler shard size text * Use eui progress labels * Fix spacer * Add beta badge * Temporarily fix scrolling * Fix grow for Top Values for * [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] Rename view mode, refactor to separate hook, add error prompt if can't show, rename wrapper, clean up & fix tests * [ML] Make doc count 0 for empty fields, update t/f test * [ML] Add unit testing for search utils * Fix missing unsubscribe for embeddable output * Remove redundant onAddFilter for this PR, fix width * Rename Field Stats to Field stats to match convention * [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 doc count for fields that exists but have no stats * [ML] Fix icon styling to match Discover but have text/keyword/histogram * [ML] Fix doc count for fields that exists but have no stats * [ML] Rename classnames to BEM style * Resolve latest changes * [ML] Add tests for data viz in Discover * Update tests & dashboard behavior to reflect new advanced settings * Update telemetry * Remove workaround after eui bump fix * Fix missing bool clause * Add login * Fix saved search attributes broken with latest changes * Update tests * Fix import * Match the no results found * Fix query util to return search source's results right away. Fix texts. * Rename old test suits to farequoteDataViewTestData Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
7f83ec09d6
commit
6600f1ad78
64 changed files with 2470 additions and 475 deletions
|
@ -10,7 +10,7 @@ exports[`FieldTypeIcon render component when type matches a field type 1`] = `
|
|||
>
|
||||
<FieldTypeIconContainer
|
||||
ariaLabel="keyword type"
|
||||
iconType="tokenKeyword"
|
||||
iconType="tokenString"
|
||||
needsAria={false}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
|
|
|
@ -48,7 +48,7 @@ export const typeToEuiIconMap: Record<string, { iconType: string; color?: string
|
|||
_source: { iconType: 'editorCodeBlock', color: 'gray' },
|
||||
string: { iconType: 'tokenString' },
|
||||
text: { iconType: 'tokenString' },
|
||||
keyword: { iconType: 'tokenKeyword' },
|
||||
keyword: { iconType: 'tokenString' },
|
||||
nested: { iconType: 'tokenNested' },
|
||||
};
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import { parse } from 'query-string';
|
||||
import { createContext, useCallback, useContext, useMemo } from 'react';
|
||||
// @ts-ignore
|
||||
import { decode } from 'rison-node';
|
||||
|
||||
export interface Dictionary<TValue> {
|
||||
|
|
|
@ -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 const DATA_VISUALIZER_GRID_EMBEDDABLE_TYPE = 'data_visualizer_grid';
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
|
||||
import { EuiLoadingSpinner, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
|
||||
export const EmbeddableLoading = () => {
|
||||
return (
|
||||
<EuiText textAlign="center">
|
||||
<EuiSpacer size="l" />
|
||||
<EuiLoadingSpinner size="l" />
|
||||
<EuiSpacer size="l" />
|
||||
</EuiText>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,234 @@
|
|||
/*
|
||||
* 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 { Observable, Subject } from 'rxjs';
|
||||
import { CoreStart } from 'kibana/public';
|
||||
import ReactDOM from 'react-dom';
|
||||
import React, { Suspense, useCallback, useState } from 'react';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { EuiEmptyPrompt, EuiIcon, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { Required } from 'utility-types';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
Embeddable,
|
||||
EmbeddableInput,
|
||||
EmbeddableOutput,
|
||||
IContainer,
|
||||
} from '../../../../../../../../src/plugins/embeddable/public';
|
||||
import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { DATA_VISUALIZER_GRID_EMBEDDABLE_TYPE } from './constants';
|
||||
import { EmbeddableLoading } from './embeddable_loading_fallback';
|
||||
import { DataVisualizerStartDependencies } from '../../../../plugin';
|
||||
import {
|
||||
IndexPattern,
|
||||
IndexPatternField,
|
||||
Query,
|
||||
} from '../../../../../../../../src/plugins/data/common';
|
||||
import { SavedSearch } from '../../../../../../../../src/plugins/discover/public';
|
||||
import {
|
||||
DataVisualizerTable,
|
||||
ItemIdToExpandedRowMap,
|
||||
} from '../../../common/components/stats_table';
|
||||
import { FieldVisConfig } from '../../../common/components/stats_table/types';
|
||||
import { getDefaultDataVisualizerListState } from '../../components/index_data_visualizer_view/index_data_visualizer_view';
|
||||
import { DataVisualizerTableState } from '../../../../../common';
|
||||
import { DataVisualizerIndexBasedAppState } from '../../types/index_data_visualizer_state';
|
||||
import { IndexBasedDataVisualizerExpandedRow } from '../../../common/components/expanded_row/index_based_expanded_row';
|
||||
import { useDataVisualizerGridData } from './use_data_visualizer_grid_data';
|
||||
|
||||
export type DataVisualizerGridEmbeddableServices = [CoreStart, DataVisualizerStartDependencies];
|
||||
export interface DataVisualizerGridEmbeddableInput extends EmbeddableInput {
|
||||
indexPattern: IndexPattern;
|
||||
savedSearch?: SavedSearch;
|
||||
query?: Query;
|
||||
visibleFieldNames?: string[];
|
||||
filters?: Filter[];
|
||||
showPreviewByDefault?: boolean;
|
||||
/**
|
||||
* Callback to add a filter to filter bar
|
||||
*/
|
||||
onAddFilter?: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
|
||||
}
|
||||
export type DataVisualizerGridEmbeddableOutput = EmbeddableOutput;
|
||||
|
||||
export type IDataVisualizerGridEmbeddable = typeof DataVisualizerGridEmbeddable;
|
||||
|
||||
const restorableDefaults = getDefaultDataVisualizerListState();
|
||||
|
||||
export const EmbeddableWrapper = ({
|
||||
input,
|
||||
onOutputChange,
|
||||
}: {
|
||||
input: DataVisualizerGridEmbeddableInput;
|
||||
onOutputChange?: (ouput: any) => void;
|
||||
}) => {
|
||||
const [dataVisualizerListState, setDataVisualizerListState] =
|
||||
useState<Required<DataVisualizerIndexBasedAppState>>(restorableDefaults);
|
||||
|
||||
const onTableChange = useCallback(
|
||||
(update: DataVisualizerTableState) => {
|
||||
setDataVisualizerListState({ ...dataVisualizerListState, ...update });
|
||||
if (onOutputChange) {
|
||||
onOutputChange(update);
|
||||
}
|
||||
},
|
||||
[dataVisualizerListState, onOutputChange]
|
||||
);
|
||||
const { configs, searchQueryLanguage, searchString, extendedColumns, loaded } =
|
||||
useDataVisualizerGridData(input, dataVisualizerListState);
|
||||
const getItemIdToExpandedRowMap = useCallback(
|
||||
function (itemIds: string[], items: FieldVisConfig[]): ItemIdToExpandedRowMap {
|
||||
return itemIds.reduce((m: ItemIdToExpandedRowMap, fieldName: string) => {
|
||||
const item = items.find((fieldVisConfig) => fieldVisConfig.fieldName === fieldName);
|
||||
if (item !== undefined) {
|
||||
m[fieldName] = (
|
||||
<IndexBasedDataVisualizerExpandedRow
|
||||
item={item}
|
||||
indexPattern={input.indexPattern}
|
||||
combinedQuery={{ searchQueryLanguage, searchString }}
|
||||
onAddFilter={input.onAddFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return m;
|
||||
}, {} as ItemIdToExpandedRowMap);
|
||||
},
|
||||
[input, searchQueryLanguage, searchString]
|
||||
);
|
||||
|
||||
if (
|
||||
loaded &&
|
||||
(configs.length === 0 ||
|
||||
// FIXME: Configs might have a placeholder document count stats field
|
||||
// This will be removed in the future
|
||||
(configs.length === 1 && configs[0].fieldName === undefined))
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
flex: '1 0 100%',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<EuiText size="xs" color="subdued">
|
||||
<EuiIcon type="visualizeApp" size="m" color="subdued" />
|
||||
<EuiSpacer size="m" />
|
||||
<FormattedMessage
|
||||
id="xpack.dataVisualizer.index.embeddableNoResultsMessage"
|
||||
defaultMessage="No results found"
|
||||
/>
|
||||
</EuiText>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<DataVisualizerTable<FieldVisConfig>
|
||||
items={configs}
|
||||
pageState={dataVisualizerListState}
|
||||
updatePageState={onTableChange}
|
||||
getItemIdToExpandedRowMap={getItemIdToExpandedRowMap}
|
||||
extendedColumns={extendedColumns}
|
||||
showPreviewByDefault={input?.showPreviewByDefault}
|
||||
onChange={onOutputChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const IndexDataVisualizerViewWrapper = (props: {
|
||||
id: string;
|
||||
embeddableContext: InstanceType<IDataVisualizerGridEmbeddable>;
|
||||
embeddableInput: Readonly<Observable<DataVisualizerGridEmbeddableInput>>;
|
||||
onOutputChange?: (output: any) => void;
|
||||
}) => {
|
||||
const { embeddableInput, onOutputChange } = props;
|
||||
|
||||
const input = useObservable(embeddableInput);
|
||||
if (input && input.indexPattern) {
|
||||
return <EmbeddableWrapper input={input} onOutputChange={onOutputChange} />;
|
||||
} else {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
iconType="alert"
|
||||
iconColor="danger"
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.dataVisualizer.index.embeddableErrorTitle"
|
||||
defaultMessage="Error loading embeddable"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.dataVisualizer.index.embeddableErrorDescription"
|
||||
defaultMessage="There was an error loading the embeddable. Please check if all the required input is valid."
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
export class DataVisualizerGridEmbeddable extends Embeddable<
|
||||
DataVisualizerGridEmbeddableInput,
|
||||
DataVisualizerGridEmbeddableOutput
|
||||
> {
|
||||
private node?: HTMLElement;
|
||||
private reload$ = new Subject();
|
||||
public readonly type: string = DATA_VISUALIZER_GRID_EMBEDDABLE_TYPE;
|
||||
|
||||
constructor(
|
||||
initialInput: DataVisualizerGridEmbeddableInput,
|
||||
public services: DataVisualizerGridEmbeddableServices,
|
||||
parent?: IContainer
|
||||
) {
|
||||
super(initialInput, {}, parent);
|
||||
}
|
||||
|
||||
public render(node: HTMLElement) {
|
||||
super.render(node);
|
||||
this.node = node;
|
||||
|
||||
const I18nContext = this.services[0].i18n.Context;
|
||||
|
||||
ReactDOM.render(
|
||||
<I18nContext>
|
||||
<KibanaContextProvider services={{ ...this.services[0], ...this.services[1] }}>
|
||||
<Suspense fallback={<EmbeddableLoading />}>
|
||||
<IndexDataVisualizerViewWrapper
|
||||
id={this.input.id}
|
||||
embeddableContext={this}
|
||||
embeddableInput={this.getInput$()}
|
||||
onOutputChange={(output) => this.updateOutput(output)}
|
||||
/>
|
||||
</Suspense>
|
||||
</KibanaContextProvider>
|
||||
</I18nContext>,
|
||||
node
|
||||
);
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
super.destroy();
|
||||
if (this.node) {
|
||||
ReactDOM.unmountComponentAtNode(this.node);
|
||||
}
|
||||
}
|
||||
|
||||
public reload() {
|
||||
this.reload$.next();
|
||||
}
|
||||
|
||||
public supportedTriggers() {
|
||||
return [];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { StartServicesAccessor } from 'kibana/public';
|
||||
import {
|
||||
EmbeddableFactoryDefinition,
|
||||
IContainer,
|
||||
} from '../../../../../../../../src/plugins/embeddable/public';
|
||||
import { DATA_VISUALIZER_GRID_EMBEDDABLE_TYPE } from './constants';
|
||||
import {
|
||||
DataVisualizerGridEmbeddableInput,
|
||||
DataVisualizerGridEmbeddableServices,
|
||||
} from './grid_embeddable';
|
||||
import { DataVisualizerPluginStart, DataVisualizerStartDependencies } from '../../../../plugin';
|
||||
|
||||
export class DataVisualizerGridEmbeddableFactory
|
||||
implements EmbeddableFactoryDefinition<DataVisualizerGridEmbeddableInput>
|
||||
{
|
||||
public readonly type = DATA_VISUALIZER_GRID_EMBEDDABLE_TYPE;
|
||||
|
||||
public readonly grouping = [
|
||||
{
|
||||
id: 'data_visualizer_grid',
|
||||
getDisplayName: () => 'Data Visualizer Grid',
|
||||
},
|
||||
];
|
||||
|
||||
constructor(
|
||||
private getStartServices: StartServicesAccessor<
|
||||
DataVisualizerStartDependencies,
|
||||
DataVisualizerPluginStart
|
||||
>
|
||||
) {}
|
||||
|
||||
public async isEditable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public canCreateNew() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public getDisplayName() {
|
||||
return i18n.translate('xpack.dataVisualizer.index.components.grid.displayName', {
|
||||
defaultMessage: 'Data visualizer grid',
|
||||
});
|
||||
}
|
||||
|
||||
public getDescription() {
|
||||
return i18n.translate('xpack.dataVisualizer.index.components.grid.description', {
|
||||
defaultMessage: 'Visualize data',
|
||||
});
|
||||
}
|
||||
|
||||
private async getServices(): Promise<DataVisualizerGridEmbeddableServices> {
|
||||
const [coreStart, pluginsStart] = await this.getStartServices();
|
||||
return [coreStart, pluginsStart];
|
||||
}
|
||||
|
||||
public async create(initialInput: DataVisualizerGridEmbeddableInput, parent?: IContainer) {
|
||||
const [coreStart, pluginsStart] = await this.getServices();
|
||||
const { DataVisualizerGridEmbeddable } = await import('./grid_embeddable');
|
||||
return new DataVisualizerGridEmbeddable(initialInput, [coreStart, pluginsStart], parent);
|
||||
}
|
||||
}
|
|
@ -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 { DataVisualizerGridEmbeddable } from './grid_embeddable';
|
||||
export { DataVisualizerGridEmbeddableFactory } from './grid_embeddable_factory';
|
|
@ -0,0 +1,587 @@
|
|||
/*
|
||||
* 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 { Required } from 'utility-types';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { merge } from 'rxjs';
|
||||
import { EuiTableActionsColumnType } from '@elastic/eui/src/components/basic_table/table_types';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { DataVisualizerIndexBasedAppState } from '../../types/index_data_visualizer_state';
|
||||
import { useDataVisualizerKibana } from '../../../kibana_context';
|
||||
import { getEsQueryFromSavedSearch } from '../../utils/saved_search_utils';
|
||||
import { MetricFieldsStats } from '../../../common/components/stats_table/components/field_count_stats';
|
||||
import { DataLoader } from '../../data_loader/data_loader';
|
||||
import { useTimefilter } from '../../hooks/use_time_filter';
|
||||
import { dataVisualizerRefresh$ } from '../../services/timefilter_refresh_service';
|
||||
import { TimeBuckets } from '../../services/time_buckets';
|
||||
import {
|
||||
DataViewField,
|
||||
KBN_FIELD_TYPES,
|
||||
UI_SETTINGS,
|
||||
} from '../../../../../../../../src/plugins/data/common';
|
||||
import { extractErrorProperties } from '../../utils/error_utils';
|
||||
import { FieldVisConfig } from '../../../common/components/stats_table/types';
|
||||
import { FieldRequestConfig, JOB_FIELD_TYPES } from '../../../../../common';
|
||||
import { kbnTypeToJobType } from '../../../common/util/field_types_utils';
|
||||
import { getActions } from '../../../common/components/field_data_row/action_menu';
|
||||
import { DataVisualizerGridEmbeddableInput } from './grid_embeddable';
|
||||
import { getDefaultPageState } from '../../components/index_data_visualizer_view/index_data_visualizer_view';
|
||||
|
||||
const defaults = getDefaultPageState();
|
||||
|
||||
export const useDataVisualizerGridData = (
|
||||
input: DataVisualizerGridEmbeddableInput,
|
||||
dataVisualizerListState: Required<DataVisualizerIndexBasedAppState>
|
||||
) => {
|
||||
const { services } = useDataVisualizerKibana();
|
||||
const { notifications, uiSettings } = services;
|
||||
const { toasts } = notifications;
|
||||
const { samplerShardSize, visibleFieldTypes, showEmptyFields } = dataVisualizerListState;
|
||||
|
||||
const [lastRefresh, setLastRefresh] = useState(0);
|
||||
|
||||
const {
|
||||
currentSavedSearch,
|
||||
currentIndexPattern,
|
||||
currentQuery,
|
||||
currentFilters,
|
||||
visibleFieldNames,
|
||||
} = useMemo(
|
||||
() => ({
|
||||
currentSavedSearch: input?.savedSearch,
|
||||
currentIndexPattern: input.indexPattern,
|
||||
currentQuery: input?.query,
|
||||
visibleFieldNames: input?.visibleFieldNames ?? [],
|
||||
currentFilters: input?.filters,
|
||||
}),
|
||||
[input]
|
||||
);
|
||||
|
||||
const { searchQueryLanguage, searchString, searchQuery } = useMemo(() => {
|
||||
const searchData = getEsQueryFromSavedSearch({
|
||||
indexPattern: currentIndexPattern,
|
||||
uiSettings,
|
||||
savedSearch: currentSavedSearch,
|
||||
query: currentQuery,
|
||||
filters: currentFilters,
|
||||
});
|
||||
|
||||
if (searchData === undefined || dataVisualizerListState.searchString !== '') {
|
||||
return {
|
||||
searchQuery: dataVisualizerListState.searchQuery,
|
||||
searchString: dataVisualizerListState.searchString,
|
||||
searchQueryLanguage: dataVisualizerListState.searchQueryLanguage,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
searchQuery: searchData.searchQuery,
|
||||
searchString: searchData.searchString,
|
||||
searchQueryLanguage: searchData.queryLanguage,
|
||||
};
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
currentSavedSearch,
|
||||
currentIndexPattern,
|
||||
dataVisualizerListState,
|
||||
currentQuery,
|
||||
currentFilters,
|
||||
]);
|
||||
|
||||
const [overallStats, setOverallStats] = useState(defaults.overallStats);
|
||||
|
||||
const [documentCountStats, setDocumentCountStats] = useState(defaults.documentCountStats);
|
||||
const [metricConfigs, setMetricConfigs] = useState(defaults.metricConfigs);
|
||||
const [metricsLoaded, setMetricsLoaded] = useState(defaults.metricsLoaded);
|
||||
const [metricsStats, setMetricsStats] = useState<undefined | MetricFieldsStats>();
|
||||
|
||||
const [nonMetricConfigs, setNonMetricConfigs] = useState(defaults.nonMetricConfigs);
|
||||
const [nonMetricsLoaded, setNonMetricsLoaded] = useState(defaults.nonMetricsLoaded);
|
||||
|
||||
const dataLoader = useMemo(
|
||||
() => new DataLoader(currentIndexPattern, toasts),
|
||||
[currentIndexPattern, toasts]
|
||||
);
|
||||
|
||||
const timefilter = useTimefilter({
|
||||
timeRangeSelector: currentIndexPattern?.timeFieldName !== undefined,
|
||||
autoRefreshSelector: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const timeUpdateSubscription = merge(
|
||||
timefilter.getTimeUpdate$(),
|
||||
dataVisualizerRefresh$
|
||||
).subscribe(() => {
|
||||
setLastRefresh(Date.now());
|
||||
});
|
||||
return () => {
|
||||
timeUpdateSubscription.unsubscribe();
|
||||
};
|
||||
});
|
||||
|
||||
const getTimeBuckets = useCallback(() => {
|
||||
return new TimeBuckets({
|
||||
[UI_SETTINGS.HISTOGRAM_MAX_BARS]: uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS),
|
||||
[UI_SETTINGS.HISTOGRAM_BAR_TARGET]: uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET),
|
||||
dateFormat: uiSettings.get('dateFormat'),
|
||||
'dateFormat:scaled': uiSettings.get('dateFormat:scaled'),
|
||||
});
|
||||
}, [uiSettings]);
|
||||
|
||||
const indexPatternFields: DataViewField[] = useMemo(
|
||||
() => currentIndexPattern.fields,
|
||||
[currentIndexPattern]
|
||||
);
|
||||
|
||||
async function loadOverallStats() {
|
||||
const tf = timefilter as any;
|
||||
let earliest;
|
||||
let latest;
|
||||
|
||||
const activeBounds = tf.getActiveBounds();
|
||||
|
||||
if (currentIndexPattern.timeFieldName !== undefined && activeBounds === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentIndexPattern.timeFieldName !== undefined) {
|
||||
earliest = activeBounds.min.valueOf();
|
||||
latest = activeBounds.max.valueOf();
|
||||
}
|
||||
|
||||
try {
|
||||
const allStats = await dataLoader.loadOverallData(
|
||||
searchQuery,
|
||||
samplerShardSize,
|
||||
earliest,
|
||||
latest
|
||||
);
|
||||
// Because load overall stats perform queries in batches
|
||||
// there could be multiple errors
|
||||
if (Array.isArray(allStats.errors) && allStats.errors.length > 0) {
|
||||
allStats.errors.forEach((err: any) => {
|
||||
dataLoader.displayError(extractErrorProperties(err));
|
||||
});
|
||||
}
|
||||
setOverallStats(allStats);
|
||||
} catch (err) {
|
||||
dataLoader.displayError(err.body ?? err);
|
||||
}
|
||||
}
|
||||
|
||||
const createMetricCards = useCallback(() => {
|
||||
const configs: FieldVisConfig[] = [];
|
||||
const aggregatableExistsFields: any[] = overallStats.aggregatableExistsFields || [];
|
||||
|
||||
const allMetricFields = indexPatternFields.filter((f) => {
|
||||
return (
|
||||
f.type === KBN_FIELD_TYPES.NUMBER &&
|
||||
f.displayName !== undefined &&
|
||||
dataLoader.isDisplayField(f.displayName) === true
|
||||
);
|
||||
});
|
||||
const metricExistsFields = allMetricFields.filter((f) => {
|
||||
return aggregatableExistsFields.find((existsF) => {
|
||||
return existsF.fieldName === f.spec.name;
|
||||
});
|
||||
});
|
||||
|
||||
// Add a config for 'document count', identified by no field name if indexpattern is time based.
|
||||
if (currentIndexPattern.timeFieldName !== undefined) {
|
||||
configs.push({
|
||||
type: JOB_FIELD_TYPES.NUMBER,
|
||||
existsInDocs: true,
|
||||
loading: true,
|
||||
aggregatable: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (metricsLoaded === false) {
|
||||
setMetricsLoaded(true);
|
||||
return;
|
||||
}
|
||||
|
||||
let aggregatableFields: any[] = overallStats.aggregatableExistsFields;
|
||||
if (allMetricFields.length !== metricExistsFields.length && metricsLoaded === true) {
|
||||
aggregatableFields = aggregatableFields.concat(overallStats.aggregatableNotExistsFields);
|
||||
}
|
||||
|
||||
const metricFieldsToShow =
|
||||
metricsLoaded === true && showEmptyFields === true ? allMetricFields : metricExistsFields;
|
||||
|
||||
metricFieldsToShow.forEach((field) => {
|
||||
const fieldData = aggregatableFields.find((f) => {
|
||||
return f.fieldName === field.spec.name;
|
||||
});
|
||||
|
||||
const metricConfig: FieldVisConfig = {
|
||||
...(fieldData ? fieldData : {}),
|
||||
fieldFormat: currentIndexPattern.getFormatterForField(field),
|
||||
type: JOB_FIELD_TYPES.NUMBER,
|
||||
loading: true,
|
||||
aggregatable: true,
|
||||
deletable: field.runtimeField !== undefined,
|
||||
};
|
||||
if (field.displayName !== metricConfig.fieldName) {
|
||||
metricConfig.displayName = field.displayName;
|
||||
}
|
||||
|
||||
configs.push(metricConfig);
|
||||
});
|
||||
|
||||
setMetricsStats({
|
||||
totalMetricFieldsCount: allMetricFields.length,
|
||||
visibleMetricsCount: metricFieldsToShow.length,
|
||||
});
|
||||
setMetricConfigs(configs);
|
||||
}, [
|
||||
currentIndexPattern,
|
||||
dataLoader,
|
||||
indexPatternFields,
|
||||
metricsLoaded,
|
||||
overallStats,
|
||||
showEmptyFields,
|
||||
]);
|
||||
|
||||
const createNonMetricCards = useCallback(() => {
|
||||
const allNonMetricFields = indexPatternFields.filter((f) => {
|
||||
return (
|
||||
f.type !== KBN_FIELD_TYPES.NUMBER &&
|
||||
f.displayName !== undefined &&
|
||||
dataLoader.isDisplayField(f.displayName) === true
|
||||
);
|
||||
});
|
||||
// Obtain the list of all non-metric fields which appear in documents
|
||||
// (aggregatable or not aggregatable).
|
||||
const populatedNonMetricFields: any[] = []; // Kibana index pattern non metric fields.
|
||||
let nonMetricFieldData: any[] = []; // Basic non metric field data loaded from requesting overall stats.
|
||||
const aggregatableExistsFields: any[] = overallStats.aggregatableExistsFields || [];
|
||||
const nonAggregatableExistsFields: any[] = overallStats.nonAggregatableExistsFields || [];
|
||||
|
||||
allNonMetricFields.forEach((f) => {
|
||||
const checkAggregatableField = aggregatableExistsFields.find(
|
||||
(existsField) => existsField.fieldName === f.spec.name
|
||||
);
|
||||
|
||||
if (checkAggregatableField !== undefined) {
|
||||
populatedNonMetricFields.push(f);
|
||||
nonMetricFieldData.push(checkAggregatableField);
|
||||
} else {
|
||||
const checkNonAggregatableField = nonAggregatableExistsFields.find(
|
||||
(existsField) => existsField.fieldName === f.spec.name
|
||||
);
|
||||
|
||||
if (checkNonAggregatableField !== undefined) {
|
||||
populatedNonMetricFields.push(f);
|
||||
nonMetricFieldData.push(checkNonAggregatableField);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (nonMetricsLoaded === false) {
|
||||
setNonMetricsLoaded(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (allNonMetricFields.length !== nonMetricFieldData.length && showEmptyFields === true) {
|
||||
// Combine the field data obtained from Elasticsearch into a single array.
|
||||
nonMetricFieldData = nonMetricFieldData.concat(
|
||||
overallStats.aggregatableNotExistsFields,
|
||||
overallStats.nonAggregatableNotExistsFields
|
||||
);
|
||||
}
|
||||
|
||||
const nonMetricFieldsToShow = showEmptyFields ? allNonMetricFields : populatedNonMetricFields;
|
||||
|
||||
const configs: FieldVisConfig[] = [];
|
||||
|
||||
nonMetricFieldsToShow.forEach((field) => {
|
||||
const fieldData = nonMetricFieldData.find((f) => f.fieldName === field.spec.name);
|
||||
|
||||
const nonMetricConfig = {
|
||||
...(fieldData ? fieldData : {}),
|
||||
fieldFormat: currentIndexPattern.getFormatterForField(field),
|
||||
aggregatable: field.aggregatable,
|
||||
scripted: field.scripted,
|
||||
loading: fieldData?.existsInDocs,
|
||||
deletable: field.runtimeField !== undefined,
|
||||
};
|
||||
|
||||
// Map the field type from the Kibana index pattern to the field type
|
||||
// used in the data visualizer.
|
||||
const dataVisualizerType = kbnTypeToJobType(field);
|
||||
if (dataVisualizerType !== undefined) {
|
||||
nonMetricConfig.type = dataVisualizerType;
|
||||
} else {
|
||||
// Add a flag to indicate that this is one of the 'other' Kibana
|
||||
// field types that do not yet have a specific card type.
|
||||
nonMetricConfig.type = field.type;
|
||||
nonMetricConfig.isUnsupportedType = true;
|
||||
}
|
||||
|
||||
if (field.displayName !== nonMetricConfig.fieldName) {
|
||||
nonMetricConfig.displayName = field.displayName;
|
||||
}
|
||||
|
||||
configs.push(nonMetricConfig);
|
||||
});
|
||||
|
||||
setNonMetricConfigs(configs);
|
||||
}, [
|
||||
currentIndexPattern,
|
||||
dataLoader,
|
||||
indexPatternFields,
|
||||
nonMetricsLoaded,
|
||||
overallStats,
|
||||
showEmptyFields,
|
||||
]);
|
||||
|
||||
async function loadMetricFieldStats() {
|
||||
// Only request data for fields that exist in documents.
|
||||
if (metricConfigs.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const configsToLoad = metricConfigs.filter(
|
||||
(config) => config.existsInDocs === true && config.loading === true
|
||||
);
|
||||
if (configsToLoad.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pass the field name, type and cardinality in the request.
|
||||
// Top values will be obtained on a sample if cardinality > 100000.
|
||||
const existMetricFields: FieldRequestConfig[] = configsToLoad.map((config) => {
|
||||
const props = { fieldName: config.fieldName, type: config.type, cardinality: 0 };
|
||||
if (config.stats !== undefined && config.stats.cardinality !== undefined) {
|
||||
props.cardinality = config.stats.cardinality;
|
||||
}
|
||||
return props;
|
||||
});
|
||||
|
||||
// Obtain the interval to use for date histogram aggregations
|
||||
// (such as the document count chart). Aim for 75 bars.
|
||||
const buckets = getTimeBuckets();
|
||||
|
||||
const tf = timefilter as any;
|
||||
let earliest: number | undefined;
|
||||
let latest: number | undefined;
|
||||
if (currentIndexPattern.timeFieldName !== undefined) {
|
||||
earliest = tf.getActiveBounds().min.valueOf();
|
||||
latest = tf.getActiveBounds().max.valueOf();
|
||||
}
|
||||
|
||||
const bounds = tf.getActiveBounds();
|
||||
const BAR_TARGET = 75;
|
||||
buckets.setInterval('auto');
|
||||
buckets.setBounds(bounds);
|
||||
buckets.setBarTarget(BAR_TARGET);
|
||||
const aggInterval = buckets.getInterval();
|
||||
|
||||
try {
|
||||
const metricFieldStats = await dataLoader.loadFieldStats(
|
||||
searchQuery,
|
||||
samplerShardSize,
|
||||
earliest,
|
||||
latest,
|
||||
existMetricFields,
|
||||
aggInterval.asMilliseconds()
|
||||
);
|
||||
|
||||
// Add the metric stats to the existing stats in the corresponding config.
|
||||
const configs: FieldVisConfig[] = [];
|
||||
metricConfigs.forEach((config) => {
|
||||
const configWithStats = { ...config };
|
||||
if (config.fieldName !== undefined) {
|
||||
configWithStats.stats = {
|
||||
...configWithStats.stats,
|
||||
...metricFieldStats.find(
|
||||
(fieldStats: any) => fieldStats.fieldName === config.fieldName
|
||||
),
|
||||
};
|
||||
configWithStats.loading = false;
|
||||
configs.push(configWithStats);
|
||||
} else {
|
||||
// Document count card.
|
||||
configWithStats.stats = metricFieldStats.find(
|
||||
(fieldStats: any) => fieldStats.fieldName === undefined
|
||||
);
|
||||
|
||||
if (configWithStats.stats !== undefined) {
|
||||
// Add earliest / latest of timefilter for setting x axis domain.
|
||||
configWithStats.stats.timeRangeEarliest = earliest;
|
||||
configWithStats.stats.timeRangeLatest = latest;
|
||||
}
|
||||
setDocumentCountStats(configWithStats);
|
||||
}
|
||||
});
|
||||
|
||||
setMetricConfigs(configs);
|
||||
} catch (err) {
|
||||
dataLoader.displayError(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadNonMetricFieldStats() {
|
||||
// Only request data for fields that exist in documents.
|
||||
if (nonMetricConfigs.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const configsToLoad = nonMetricConfigs.filter(
|
||||
(config) => config.existsInDocs === true && config.loading === true
|
||||
);
|
||||
if (configsToLoad.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pass the field name, type and cardinality in the request.
|
||||
// Top values will be obtained on a sample if cardinality > 100000.
|
||||
const existNonMetricFields: FieldRequestConfig[] = configsToLoad.map((config) => {
|
||||
const props = { fieldName: config.fieldName, type: config.type, cardinality: 0 };
|
||||
if (config.stats !== undefined && config.stats.cardinality !== undefined) {
|
||||
props.cardinality = config.stats.cardinality;
|
||||
}
|
||||
return props;
|
||||
});
|
||||
|
||||
const tf = timefilter as any;
|
||||
let earliest;
|
||||
let latest;
|
||||
if (currentIndexPattern.timeFieldName !== undefined) {
|
||||
earliest = tf.getActiveBounds().min.valueOf();
|
||||
latest = tf.getActiveBounds().max.valueOf();
|
||||
}
|
||||
|
||||
try {
|
||||
const nonMetricFieldStats = await dataLoader.loadFieldStats(
|
||||
searchQuery,
|
||||
samplerShardSize,
|
||||
earliest,
|
||||
latest,
|
||||
existNonMetricFields
|
||||
);
|
||||
|
||||
// Add the field stats to the existing stats in the corresponding config.
|
||||
const configs: FieldVisConfig[] = [];
|
||||
nonMetricConfigs.forEach((config) => {
|
||||
const configWithStats = { ...config };
|
||||
if (config.fieldName !== undefined) {
|
||||
configWithStats.stats = {
|
||||
...configWithStats.stats,
|
||||
...nonMetricFieldStats.find(
|
||||
(fieldStats: any) => fieldStats.fieldName === config.fieldName
|
||||
),
|
||||
};
|
||||
}
|
||||
configWithStats.loading = false;
|
||||
configs.push(configWithStats);
|
||||
});
|
||||
|
||||
setNonMetricConfigs(configs);
|
||||
} catch (err) {
|
||||
dataLoader.displayError(err);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadOverallStats();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [searchQuery, samplerShardSize, lastRefresh]);
|
||||
|
||||
useEffect(() => {
|
||||
createMetricCards();
|
||||
createNonMetricCards();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [overallStats, showEmptyFields]);
|
||||
|
||||
useEffect(() => {
|
||||
loadMetricFieldStats();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [metricConfigs]);
|
||||
|
||||
useEffect(() => {
|
||||
loadNonMetricFieldStats();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [nonMetricConfigs]);
|
||||
|
||||
useEffect(() => {
|
||||
createMetricCards();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [metricsLoaded]);
|
||||
|
||||
useEffect(() => {
|
||||
createNonMetricCards();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [nonMetricsLoaded]);
|
||||
|
||||
const configs = useMemo(() => {
|
||||
let combinedConfigs = [...nonMetricConfigs, ...metricConfigs];
|
||||
if (visibleFieldTypes && visibleFieldTypes.length > 0) {
|
||||
combinedConfigs = combinedConfigs.filter(
|
||||
(config) => visibleFieldTypes.findIndex((field) => field === config.type) > -1
|
||||
);
|
||||
}
|
||||
if (visibleFieldNames && visibleFieldNames.length > 0) {
|
||||
combinedConfigs = combinedConfigs.filter(
|
||||
(config) => visibleFieldNames.findIndex((field) => field === config.fieldName) > -1
|
||||
);
|
||||
}
|
||||
|
||||
return combinedConfigs;
|
||||
}, [nonMetricConfigs, metricConfigs, visibleFieldTypes, visibleFieldNames]);
|
||||
|
||||
// Some actions open up fly-out or popup
|
||||
// This variable is used to keep track of them and clean up when unmounting
|
||||
const actionFlyoutRef = useRef<() => void | undefined>();
|
||||
useEffect(() => {
|
||||
const ref = actionFlyoutRef;
|
||||
return () => {
|
||||
// Clean up any of the flyout/editor opened from the actions
|
||||
if (ref.current) {
|
||||
ref.current();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Inject custom action column for the index based visualizer
|
||||
// Hide the column completely if no access to any of the plugins
|
||||
const extendedColumns = useMemo(() => {
|
||||
const actions = getActions(
|
||||
input.indexPattern,
|
||||
{ lens: services.lens },
|
||||
{
|
||||
searchQueryLanguage,
|
||||
searchString,
|
||||
},
|
||||
actionFlyoutRef
|
||||
);
|
||||
if (!Array.isArray(actions) || actions.length < 1) return;
|
||||
|
||||
const actionColumn: EuiTableActionsColumnType<FieldVisConfig> = {
|
||||
name: i18n.translate('xpack.dataVisualizer.index.dataGrid.actionsColumnLabel', {
|
||||
defaultMessage: 'Actions',
|
||||
}),
|
||||
actions,
|
||||
width: '70px',
|
||||
};
|
||||
|
||||
return [actionColumn];
|
||||
}, [input.indexPattern, services, searchQueryLanguage, searchString]);
|
||||
|
||||
return {
|
||||
configs,
|
||||
searchQueryLanguage,
|
||||
searchString,
|
||||
searchQuery,
|
||||
extendedColumns,
|
||||
documentCountStats,
|
||||
metricsStats,
|
||||
loaded: metricsLoaded && nonMetricsLoaded,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 { CoreSetup } from 'kibana/public';
|
||||
import { EmbeddableSetup } from '../../../../../../../src/plugins/embeddable/public';
|
||||
import { DataVisualizerGridEmbeddableFactory } from './grid_embeddable/grid_embeddable_factory';
|
||||
import { DataVisualizerPluginStart, DataVisualizerStartDependencies } from '../../../plugin';
|
||||
|
||||
export function registerEmbeddables(
|
||||
embeddable: EmbeddableSetup,
|
||||
core: CoreSetup<DataVisualizerStartDependencies, DataVisualizerPluginStart>
|
||||
) {
|
||||
const dataVisualizerGridEmbeddableFactory = new DataVisualizerGridEmbeddableFactory(
|
||||
core.getStartServices
|
||||
);
|
||||
embeddable.registerEmbeddableFactory(
|
||||
dataVisualizerGridEmbeddableFactory.type,
|
||||
dataVisualizerGridEmbeddableFactory
|
||||
);
|
||||
}
|
|
@ -9,7 +9,6 @@ import React, { FC, useCallback, useEffect, useState } from 'react';
|
|||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { parse, stringify } from 'query-string';
|
||||
import { isEqual } from 'lodash';
|
||||
// @ts-ignore
|
||||
import { encode } from 'rison-node';
|
||||
import { SimpleSavedObject } from 'kibana/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -29,7 +28,7 @@ import {
|
|||
isRisonSerializationRequired,
|
||||
} from '../common/util/url_state';
|
||||
import { useDataVisualizerKibana } from '../kibana_context';
|
||||
import { IndexPattern } from '../../../../../../src/plugins/data/common';
|
||||
import { DataView } from '../../../../../../src/plugins/data/common';
|
||||
import { ResultLink } from '../common/components/results_links';
|
||||
|
||||
export type IndexDataVisualizerSpec = typeof IndexDataVisualizer;
|
||||
|
@ -51,9 +50,7 @@ export const DataVisualizerUrlStateContextProvider: FC<DataVisualizerUrlStateCon
|
|||
const history = useHistory();
|
||||
const { search: searchString } = useLocation();
|
||||
|
||||
const [currentIndexPattern, setCurrentIndexPattern] = useState<IndexPattern | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [currentIndexPattern, setCurrentIndexPattern] = useState<DataView | undefined>(undefined);
|
||||
const [currentSavedSearch, setCurrentSavedSearch] = useState<SimpleSavedObject<unknown> | null>(
|
||||
null
|
||||
);
|
||||
|
|
|
@ -4,8 +4,6 @@
|
|||
* 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';
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
getQueryFromSavedSearch,
|
||||
getQueryFromSavedSearchObject,
|
||||
createMergedEsQuery,
|
||||
getEsQueryFromSavedSearch,
|
||||
} from './saved_search_utils';
|
||||
|
@ -82,9 +82,9 @@ const kqlSavedSearch: SavedSearch = {
|
|||
},
|
||||
};
|
||||
|
||||
describe('getQueryFromSavedSearch()', () => {
|
||||
describe('getQueryFromSavedSearchObject()', () => {
|
||||
it('should return parsed searchSourceJSON with query and filter', () => {
|
||||
expect(getQueryFromSavedSearch(luceneSavedSearchObj)).toEqual({
|
||||
expect(getQueryFromSavedSearchObject(luceneSavedSearchObj)).toEqual({
|
||||
filter: [
|
||||
{
|
||||
$state: { store: 'appState' },
|
||||
|
@ -106,7 +106,7 @@ describe('getQueryFromSavedSearch()', () => {
|
|||
query: { language: 'lucene', query: 'responsetime:>50' },
|
||||
version: true,
|
||||
});
|
||||
expect(getQueryFromSavedSearch(kqlSavedSearch)).toEqual({
|
||||
expect(getQueryFromSavedSearchObject(kqlSavedSearch)).toEqual({
|
||||
filter: [
|
||||
{
|
||||
$state: { store: 'appState' },
|
||||
|
@ -130,7 +130,7 @@ describe('getQueryFromSavedSearch()', () => {
|
|||
});
|
||||
});
|
||||
it('should return undefined if invalid searchSourceJSON', () => {
|
||||
expect(getQueryFromSavedSearch(luceneInvalidSavedSearchObj)).toEqual(undefined);
|
||||
expect(getQueryFromSavedSearchObject(luceneInvalidSavedSearchObj)).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -16,17 +16,31 @@ import {
|
|||
Filter,
|
||||
} from '@kbn/es-query';
|
||||
import { isSavedSearchSavedObject, SavedSearchSavedObject } from '../../../../common/types';
|
||||
import { IndexPattern } from '../../../../../../../src/plugins/data/common';
|
||||
import { IndexPattern, SearchSource } from '../../../../../../../src/plugins/data/common';
|
||||
import { SEARCH_QUERY_LANGUAGE, SearchQueryLanguage } from '../types/combined_query';
|
||||
import { SavedSearch } from '../../../../../../../src/plugins/discover/public';
|
||||
import { getEsQueryConfig } from '../../../../../../../src/plugins/data/common';
|
||||
import { FilterManager } from '../../../../../../../src/plugins/data/public';
|
||||
|
||||
const DEFAULT_QUERY = {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
match_all: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export function getDefaultQuery() {
|
||||
return cloneDeep(DEFAULT_QUERY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the stringified searchSourceJSON
|
||||
* from a saved search or saved search object
|
||||
*/
|
||||
export function getQueryFromSavedSearch(savedSearch: SavedSearchSavedObject | SavedSearch) {
|
||||
export function getQueryFromSavedSearchObject(savedSearch: SavedSearchSavedObject | SavedSearch) {
|
||||
const search = isSavedSearchSavedObject(savedSearch)
|
||||
? savedSearch?.attributes?.kibanaSavedObjectMeta
|
||||
: // @ts-expect-error kibanaSavedObjectMeta does exist
|
||||
|
@ -69,20 +83,22 @@ export function createMergedEsQuery(
|
|||
if (query.query !== '') {
|
||||
combinedQuery = toElasticsearchQuery(ast, indexPattern);
|
||||
}
|
||||
const filterQuery = buildQueryFromFilters(filters, indexPattern);
|
||||
if (combinedQuery.bool !== undefined) {
|
||||
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.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];
|
||||
}
|
||||
|
||||
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,
|
||||
|
@ -115,10 +131,31 @@ export function getEsQueryFromSavedSearch({
|
|||
}) {
|
||||
if (!indexPattern || !savedSearch) return;
|
||||
|
||||
const savedSearchData = getQueryFromSavedSearch(savedSearch);
|
||||
const userQuery = query;
|
||||
const userFilters = filters;
|
||||
|
||||
// If saved search has a search source with nested parent
|
||||
// e.g. a search coming from Dashboard saved search embeddable
|
||||
// which already combines both the saved search's original query/filters and the Dashboard's
|
||||
// then no need to process any further
|
||||
if (
|
||||
savedSearch &&
|
||||
'searchSource' in savedSearch &&
|
||||
savedSearch?.searchSource instanceof SearchSource &&
|
||||
savedSearch.searchSource.getParent() !== undefined &&
|
||||
userQuery
|
||||
) {
|
||||
return {
|
||||
searchQuery: savedSearch.searchSource.getSearchRequestBody()?.query ?? getDefaultQuery(),
|
||||
searchString: userQuery.query,
|
||||
queryLanguage: userQuery.language as SearchQueryLanguage,
|
||||
};
|
||||
}
|
||||
|
||||
// If saved search is an json object with the original query and filter
|
||||
// retrieve the parsed query and filter
|
||||
const savedSearchData = getQueryFromSavedSearchObject(savedSearch);
|
||||
|
||||
// If no saved search available, use user's query and filters
|
||||
if (!savedSearchData && userQuery) {
|
||||
if (filterManager && userFilters) filterManager.setFilters(userFilters);
|
||||
|
@ -137,7 +174,8 @@ export function getEsQueryFromSavedSearch({
|
|||
};
|
||||
}
|
||||
|
||||
// If saved search available, merge saved search with latest user query or filters differ from extracted saved search data
|
||||
// If saved search available, merge saved search with latest user query or filters
|
||||
// which might differ from extracted saved search data
|
||||
if (savedSearchData) {
|
||||
const currentQuery = userQuery ?? savedSearchData?.query;
|
||||
const currentFilters = userFilters ?? savedSearchData?.filter;
|
||||
|
@ -158,17 +196,3 @@ export function getEsQueryFromSavedSearch({
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_QUERY = {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
match_all: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export function getDefaultQuery() {
|
||||
return cloneDeep(DEFAULT_QUERY);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { CoreSetup, CoreStart } from 'kibana/public';
|
||||
import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public';
|
||||
import type { EmbeddableSetup, EmbeddableStart } from '../../../../src/plugins/embeddable/public';
|
||||
import type { SharePluginStart } from '../../../../src/plugins/share/public';
|
||||
import { Plugin } from '../../../../src/core/public';
|
||||
|
||||
|
@ -21,9 +21,11 @@ import type { IndexPatternFieldEditorStart } from '../../../../src/plugins/index
|
|||
import { getFileDataVisualizerComponent, getIndexDataVisualizerComponent } from './api';
|
||||
import { getMaxBytesFormatted } from './application/common/util/get_max_bytes';
|
||||
import { registerHomeAddData, registerHomeFeatureCatalogue } from './register_home';
|
||||
import { registerEmbeddables } from './application/index_data_visualizer/embeddables';
|
||||
|
||||
export interface DataVisualizerSetupDependencies {
|
||||
home?: HomePublicPluginSetup;
|
||||
embeddable: EmbeddableSetup;
|
||||
}
|
||||
export interface DataVisualizerStartDependencies {
|
||||
data: DataPublicPluginStart;
|
||||
|
@ -56,6 +58,9 @@ export class DataVisualizerPlugin
|
|||
registerHomeAddData(plugins.home);
|
||||
registerHomeFeatureCatalogue(plugins.home);
|
||||
}
|
||||
if (plugins.embeddable) {
|
||||
registerEmbeddables(plugins.embeddable, core);
|
||||
}
|
||||
}
|
||||
|
||||
public start(core: CoreStart, plugins: DataVisualizerStartDependencies) {
|
||||
|
|
|
@ -6,9 +6,18 @@
|
|||
"declaration": true,
|
||||
"declarationMap": true
|
||||
},
|
||||
"include": ["common/**/*", "public/**/*", "server/**/*"],
|
||||
"include": [
|
||||
"../../../typings/**/*",
|
||||
"common/**/*",
|
||||
"public/**/*",
|
||||
"scripts/**/*",
|
||||
"server/**/*",
|
||||
"types/**/*"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../../../src/core/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/kibana_utils/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/kibana_react/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/data/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/usage_collection/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/custom_integrations/tsconfig.json" },
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue