[ML] Enhance support for ES|QL Data visualizer (#176515)

## Summary

This PR enhances support for ES|QL data visualizer. Changes include:
- Add an Update button that when clicked, will update and run the query.
This is to complement the current cmd + Enter keyboard short cut.


5ca3ac0b-782e-404c-a04b-330c8eea6ab7


- Improve logic to no longer fetch total count & document count if only
the limit size is updated (so changing the limit size, but not the query
or time, will not refresh the count chart again)

- Remove dependency from data view's field format
- Refactor into a data fetching & processing into common hook to be used
for embeddable

- Support ES|QL in Field stats embeddable 

- Fix count % of documents where field exists is > 100% when there are
multi-field values. (E.g. when row is an array of values like ["a", "b",
"c"], the count is much higher than the total number of rows)

<img width="1492" alt="Screenshot 2024-02-09 at 12 48 13"
src="c437e4f9-10e4-4d26-b00a-57277c5e1287">

- Add support for `geo_point` and `geo_shape` field types

<img width="1492" alt="Screenshot 2024-02-09 at 12 47 37"
src="d72e1e73-9880-4a12-be65-29b569d80694">


### Checklist

Delete any items that are not applicable to this PR.

- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)


### Risk Matrix

Delete this section if it is not applicable to this PR.

Before closing this PR, invite QA, stakeholders, and other developers to
identify risks that should be tested prior to the change/feature
release.

When forming the risk matrix, consider some of the following examples
and how they may potentially impact the change:

| Risk | Probability | Severity | Mitigation/Notes |

|---------------------------|-------------|----------|-------------------------|
| Multiple Spaces&mdash;unexpected behavior in non-default Kibana Space.
| Low | High | Integration tests will verify that all features are still
supported in non-default Kibana Space and when user switches between
spaces. |
| Multiple nodes&mdash;Elasticsearch polling might have race conditions
when multiple Kibana nodes are polling for the same tasks. | High | Low
| Tasks are idempotent, so executing them multiple times will not result
in logical error, but will degrade performance. To test for this case we
add plenty of unit tests around this logic and document manual testing
procedure. |
| Code should gracefully handle cases when feature X or plugin Y are
disabled. | Medium | High | Unit tests will verify that any feature flag
or plugin combination still results in our service operational. |
| [See more potential risk
examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) |


### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Quynh Nguyen (Quinn) 2024-02-12 17:04:46 -06:00 committed by GitHub
parent 3b08e74e58
commit 6010b64204
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 1317 additions and 865 deletions

View file

@ -94,6 +94,15 @@ interface DatePickerWrapperProps {
* Boolean flag to disable the date picker
*/
isDisabled?: boolean;
/**
* Boolean flag to force change from 'Refresh' to 'Update' state
*/
needsUpdate?: boolean;
/**
* Callback function that gets called
* when EuiSuperDatePicker's 'Refresh'|'Update' button is clicked
*/
onRefresh?: () => void;
}
/**
@ -111,6 +120,8 @@ export const DatePickerWrapper: FC<DatePickerWrapperProps> = (props) => {
width,
flexGroup = true,
isDisabled = false,
needsUpdate,
onRefresh,
} = props;
const {
data,
@ -285,6 +296,12 @@ export const DatePickerWrapper: FC<DatePickerWrapperProps> = (props) => {
setRefreshInterval({ pause, value });
}
const handleRefresh = useCallback(() => {
updateLastRefresh();
if (onRefresh) {
onRefresh();
}
}, [onRefresh]);
const flexItems = (
<>
<EuiFlexItem>
@ -296,12 +313,16 @@ export const DatePickerWrapper: FC<DatePickerWrapperProps> = (props) => {
isAutoRefreshOnly={!isTimeRangeSelectorEnabled || isAutoRefreshOnly}
refreshInterval={refreshInterval.value || DEFAULT_REFRESH_INTERVAL_MS}
onTimeChange={updateTimeFilter}
onRefresh={updateLastRefresh}
onRefresh={handleRefresh}
onRefreshChange={updateInterval}
recentlyUsedRanges={recentlyUsedRanges}
dateFormat={dateFormat}
commonlyUsedRanges={commonlyUsedRanges}
updateButtonProps={{ iconOnly: isWithinLBreakpoint, fill: false }}
updateButtonProps={{
iconOnly: isWithinLBreakpoint,
fill: false,
...(needsUpdate ? { needsUpdate } : {}),
}}
width={width}
isDisabled={isDisabled}
/>
@ -310,13 +331,20 @@ export const DatePickerWrapper: FC<DatePickerWrapperProps> = (props) => {
<EuiFlexItem grow={false}>
<EuiButton
fill={false}
color="primary"
iconType={'refresh'}
onClick={() => updateLastRefresh()}
color={needsUpdate ? 'success' : 'primary'}
iconType={needsUpdate ? 'kqlFunction' : 'refresh'}
onClick={handleRefresh}
data-test-subj={`mlDatePickerRefreshPageButton${isLoading ? ' loading' : ' loaded'}`}
isLoading={isLoading}
>
<FormattedMessage id="xpack.ml.datePicker.pageRefreshButton" defaultMessage="Refresh" />
{needsUpdate ? (
<FormattedMessage id="xpack.ml.datePicker.pageUpdateButton" defaultMessage="Update" />
) : (
<FormattedMessage
id="xpack.ml.datePicker.pageRefreshButton"
defaultMessage="Refresh"
/>
)}
</EuiButton>
</EuiFlexItem>
) : null}

View file

@ -5,13 +5,13 @@
* 2.0.
*/
import React, { FC, useEffect, useState } from 'react';
import { DataView } from '@kbn/data-views-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import { ES_GEO_FIELD_TYPE, LayerDescriptor } from '@kbn/maps-plugin/common';
import { CombinedQuery } from '../../../../index_data_visualizer/types/combined_query';
import type { CombinedQuery } from '../../../../index_data_visualizer/types/combined_query';
import { ExpandedRowContent } from '../../stats_table/components/field_data_expanded_row/expanded_row_content';
import { DocumentStatsTable } from '../../stats_table/components/field_data_expanded_row/document_stats';
import { ExamplesList } from '../../examples_list';
import { FieldVisConfig } from '../../stats_table/types';
import type { FieldVisConfig } from '../../stats_table/types';
import { useDataVisualizerKibana } from '../../../../kibana_context';
import { SUPPORTED_FIELD_TYPES } from '../../../../../../common/constants';
import { EmbeddedMapComponent } from '../../embedded_map';
@ -20,8 +20,9 @@ import { ExpandedRowPanel } from '../../stats_table/components/field_data_expand
export const GeoPointContentWithMap: FC<{
config: FieldVisConfig;
dataView: DataView | undefined;
combinedQuery: CombinedQuery;
}> = ({ config, dataView, combinedQuery }) => {
combinedQuery?: CombinedQuery;
esql?: string;
}> = ({ config, dataView, combinedQuery, esql }) => {
const { stats } = config;
const [layerList, setLayerList] = useState<LayerDescriptor[]>([]);
const {
@ -43,22 +44,60 @@ export const GeoPointContentWithMap: FC<{
geoFieldName: config.fieldName,
geoFieldType: config.type as ES_GEO_FIELD_TYPE,
filters: data.query.filterManager.getFilters() ?? [],
query: {
query: combinedQuery.searchString,
language: combinedQuery.searchQueryLanguage,
},
...(typeof esql === 'string' ? { esql, type: 'ESQL' } : {}),
...(combinedQuery
? {
query: {
query: combinedQuery.searchString,
language: combinedQuery.searchQueryLanguage,
},
}
: {}),
};
const searchLayerDescriptor = mapsPlugin
? await mapsPlugin.createLayerDescriptors.createESSearchSourceLayerDescriptor(params)
: null;
if (searchLayerDescriptor) {
setLayerList([...layerList, searchLayerDescriptor]);
if (searchLayerDescriptor?.sourceDescriptor) {
if (esql !== undefined) {
// Currently, createESSearchSourceLayerDescriptor doesn't support ES|QL yet
// but we can manually override the source descriptor with the ES|QL ESQLSourceDescriptor
const esqlSourceDescriptor = {
columns: [
{
name: config.fieldName,
type: config.type,
},
],
dataViewId: dataView.id,
dateField: dataView.timeFieldName,
geoField: config.fieldName,
esql,
narrowByGlobalSearch: true,
narrowByGlobalTime: true,
narrowByMapBounds: true,
id: searchLayerDescriptor.sourceDescriptor.id,
type: 'ESQL',
applyForceRefresh: true,
};
setLayerList([
...layerList,
{
...searchLayerDescriptor,
sourceDescriptor: esqlSourceDescriptor,
},
]);
} else {
setLayerList([...layerList, searchLayerDescriptor]);
}
}
}
}
updateIndexPatternSearchLayer();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dataView, combinedQuery, config, mapsPlugin, data.query]);
}, [dataView, combinedQuery, esql, config, mapsPlugin, data.query]);
if (stats?.examples === undefined) return null;
return (

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import { DataView, DataViewField } from '@kbn/data-views-plugin/public';
import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
import { useExpandedRowCss } from './use_expanded_row_css';
import { GeoPointContentWithMap } from './geo_point_content_with_map';
import { SUPPORTED_FIELD_TYPES } from '../../../../../common/constants';
@ -20,8 +20,8 @@ import {
TextContent,
} from '../stats_table/components/field_data_expanded_row';
import { NotInDocsContent } from '../not_in_docs_content';
import { FieldVisConfig } from '../stats_table/types';
import { CombinedQuery } from '../../../index_data_visualizer/types/combined_query';
import type { FieldVisConfig } from '../stats_table/types';
import type { CombinedQuery } from '../../../index_data_visualizer/types/combined_query';
import { LoadingIndicator } from '../loading_indicator';
import { ErrorMessageContent } from '../stats_table/components/field_data_expanded_row/error_message';
@ -30,12 +30,14 @@ export const IndexBasedDataVisualizerExpandedRow = ({
dataView,
combinedQuery,
onAddFilter,
esql,
totalDocuments,
typeAccessor = 'type',
}: {
item: FieldVisConfig;
dataView: DataView | undefined;
combinedQuery: CombinedQuery;
combinedQuery?: CombinedQuery;
esql?: string;
totalDocuments?: number;
typeAccessor?: 'type' | 'secondaryType';
/**
@ -74,6 +76,7 @@ export const IndexBasedDataVisualizerExpandedRow = ({
config={config}
dataView={dataView}
combinedQuery={combinedQuery}
esql={esql}
/>
);

View file

@ -7,14 +7,14 @@
import { i18n } from '@kbn/i18n';
import { Action } from '@elastic/eui/src/components/basic_table/action_types';
import { MutableRefObject } from 'react';
import { DataView } from '@kbn/data-views-plugin/public';
import type { MutableRefObject } from 'react';
import type { DataView } from '@kbn/data-views-plugin/public';
import { VISUALIZE_GEO_FIELD_TRIGGER } from '@kbn/ui-actions-plugin/public';
import { mlTimefilterRefresh$, Refresh } from '@kbn/ml-date-picker';
import { getCompatibleLensDataType, getLensAttributes } from './lens_utils';
import { CombinedQuery } from '../../../../index_data_visualizer/types/combined_query';
import { FieldVisConfig } from '../../stats_table/types';
import { DataVisualizerKibanaReactContextValue } from '../../../../kibana_context';
import type { CombinedQuery } from '../../../../index_data_visualizer/types/combined_query';
import type { FieldVisConfig } from '../../stats_table/types';
import type { DataVisualizerKibanaReactContextValue } from '../../../../kibana_context';
import { SUPPORTED_FIELD_TYPES } from '../../../../../../common/constants';
import { APP_ID } from '../../../../../../common/constants';

View file

@ -32,7 +32,7 @@ export const DocumentStat = ({ config, showIcon, totalCount }: Props) => {
const { count, sampleCount } = stats;
const total = sampleCount ?? totalCount;
const total = Math.min(sampleCount ?? Infinity, totalCount ?? Infinity);
// 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%

View file

@ -8,7 +8,7 @@
import React, { FC, useMemo } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { MetricDistributionChart, buildChartDataFromStats } from '../metric_distribution_chart';
import { FieldVisConfig } from '../../types';
import type { FieldVisConfig } from '../../types';
import { kibanaFieldFormat, formatSingleValue } from '../../../utils';
const METRIC_DISTRIBUTION_CHART_WIDTH = 100;

View file

@ -126,7 +126,7 @@ export const TopValues: FC<Props> = ({ stats, fieldFormat, barColor, compressed,
max={1}
color={barColor}
size="xs"
label={kibanaFieldFormat(value.key, fieldFormat)}
label={value.key ? kibanaFieldFormat(value.key, fieldFormat) : fieldValue}
className={classNames('eui-textTruncate', 'topValuesValueLabelContainer')}
valueText={`${value.doc_count}${
totalDocuments !== undefined

View file

@ -6,20 +6,13 @@
*/
/* eslint-disable react-hooks/exhaustive-deps */
import { css } from '@emotion/react';
import React, { FC, useEffect, useMemo, useState, useCallback, useRef } from 'react';
import type { Required } from 'utility-types';
import {
FullTimeRangeSelector,
mlTimefilterRefresh$,
useTimefilter,
DatePickerWrapper,
} from '@kbn/ml-date-picker';
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { usePageUrlState } from '@kbn/ml-url-state';
import { FullTimeRangeSelector, DatePickerWrapper } from '@kbn/ml-date-picker';
import { TextBasedLangEditor } from '@kbn/text-based-languages/public';
import type { AggregateQuery } from '@kbn/es-query';
import { merge } from 'rxjs';
import { Comparators } from '@elastic/eui';
import {
useEuiBreakpoint,
@ -31,112 +24,28 @@ import {
EuiProgress,
EuiSpacer,
} from '@elastic/eui';
import { usePageUrlState, useUrlState } from '@kbn/ml-url-state';
import { SEARCH_QUERY_LANGUAGE } from '@kbn/ml-query-utils';
import { getIndexPatternFromSQLQuery, getIndexPatternFromESQLQuery } from '@kbn/esql-utils';
import type { DataView } from '@kbn/data-views-plugin/common';
import { KBN_FIELD_TYPES } from '@kbn/field-types';
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { getFieldType } from '@kbn/field-utils';
import { UI_SETTINGS } from '@kbn/data-service';
import type { SupportedFieldType } from '../../../../../common/types';
import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils';
import { getOrCreateDataViewByIndexPattern } from '../../search_strategy/requests/get_data_view_by_index_pattern';
import { useCurrentEuiTheme } from '../../../common/hooks/use_current_eui_theme';
import type { FieldVisConfig } from '../../../common/components/stats_table/types';
import { DATA_VISUALIZER_INDEX_VIEWER } from '../../constants/index_data_visualizer_viewer';
import type { DataVisualizerIndexBasedAppState } from '../../types/index_data_visualizer_state';
import { useDataVisualizerKibana } from '../../../kibana_context';
import { GetAdditionalLinks } from '../../../common/components/results_links';
import { DocumentCountContent } from '../../../common/components/document_count_content';
import { useTimeBuckets } from '../../../common/hooks/use_time_buckets';
import {
DataVisualizerTable,
ItemIdToExpandedRowMap,
} from '../../../common/components/stats_table';
import type {
MetricFieldsStats,
TotalFieldsStats,
} from '../../../common/components/stats_table/components/field_count_stats';
import { filterFields } from '../../../common/components/fields_stats_grid/filter_fields';
import { IndexBasedDataVisualizerExpandedRow } from '../../../common/components/expanded_row/index_based_expanded_row';
import { getOrCreateDataViewByIndexPattern } from '../../search_strategy/requests/get_data_view_by_index_pattern';
import { DataVisualizerTable } from '../../../common/components/stats_table';
import { FieldCountPanel } from '../../../common/components/field_count_panel';
import { useESQLFieldStatsData } from '../../hooks/esql/use_esql_field_stats_data';
import type { NonAggregatableField, OverallStats } from '../../types/overall_stats';
import { isESQLQuery } from '../../search_strategy/requests/esql_utils';
import { DEFAULT_BAR_TARGET } from '../../../common/constants';
import { ESQLDefaultLimitSizeSelect } from '../search_panel/esql/limit_size';
import {
type ESQLDefaultLimitSizeOption,
ESQLDefaultLimitSizeSelect,
} from '../search_panel/esql/limit_size';
import { type Column, useESQLOverallStatsData } from '../../hooks/esql/use_esql_overall_stats_data';
import { type AggregatableField } from '../../types/esql_data_visualizer';
const defaults = getDefaultPageState();
interface DataVisualizerPageState {
overallStats: OverallStats;
metricConfigs: FieldVisConfig[];
totalMetricFieldCount: number;
populatedMetricFieldCount: number;
metricsLoaded: boolean;
nonMetricConfigs: FieldVisConfig[];
nonMetricsLoaded: boolean;
documentCountStats?: FieldVisConfig;
}
const defaultSearchQuery = {
match_all: {},
};
export function getDefaultPageState(): DataVisualizerPageState {
return {
overallStats: {
totalCount: 0,
aggregatableExistsFields: [],
aggregatableNotExistsFields: [],
nonAggregatableExistsFields: [],
nonAggregatableNotExistsFields: [],
},
metricConfigs: [],
totalMetricFieldCount: 0,
populatedMetricFieldCount: 0,
metricsLoaded: false,
nonMetricConfigs: [],
nonMetricsLoaded: false,
documentCountStats: undefined,
};
}
interface ESQLDataVisualizerIndexBasedAppState extends DataVisualizerIndexBasedAppState {
limitSize: ESQLDefaultLimitSizeOption;
}
export interface ESQLDataVisualizerIndexBasedPageUrlState {
pageKey: typeof DATA_VISUALIZER_INDEX_VIEWER;
pageUrlState: Required<ESQLDataVisualizerIndexBasedAppState>;
}
export const getDefaultDataVisualizerListState = (
overrides?: Partial<ESQLDataVisualizerIndexBasedAppState>
): Required<ESQLDataVisualizerIndexBasedAppState> => ({
pageIndex: 0,
pageSize: 25,
sortField: 'fieldName',
sortDirection: 'asc',
visibleFieldTypes: [],
visibleFieldNames: [],
limitSize: '10000',
searchString: '',
searchQuery: defaultSearchQuery,
searchQueryLanguage: SEARCH_QUERY_LANGUAGE.KUERY,
filters: [],
showDistributions: true,
showAllFields: false,
showEmptyFields: false,
probability: null,
rndSamplerPref: 'off',
...overrides,
});
getDefaultESQLDataVisualizerListState,
useESQLDataVisualizerData,
} from '../../hooks/esql/use_data_visualizer_esql_data';
import type {
DataVisualizerGridInput,
ESQLDataVisualizerIndexBasedPageUrlState,
ESQLDefaultLimitSizeOption,
} from '../../embeddables/grid_embeddable/types';
import { ESQLQuery, isESQLQuery } from '../../search_strategy/requests/esql_utils';
export interface IndexDataVisualizerESQLProps {
getAdditionalLinks?: GetAdditionalLinks;
@ -144,42 +53,27 @@ export interface IndexDataVisualizerESQLProps {
export const IndexDataVisualizerESQL: FC<IndexDataVisualizerESQLProps> = (dataVisualizerProps) => {
const { services } = useDataVisualizerKibana();
const { data, fieldFormats, uiSettings } = services;
const { data } = services;
const euiTheme = useCurrentEuiTheme();
const [query, setQuery] = useState<AggregateQuery>({ esql: '' });
const [query, setQuery] = useState<ESQLQuery>({ esql: '' });
const [currentDataView, setCurrentDataView] = useState<DataView | undefined>();
const updateDataView = (dv: DataView) => {
if (dv.id !== currentDataView?.id) {
setCurrentDataView(dv);
}
const toggleShowEmptyFields = () => {
setDataVisualizerListState({
...dataVisualizerListState,
showEmptyFields: !dataVisualizerListState.showEmptyFields,
});
};
const updateLimitSize = (newLimitSize: ESQLDefaultLimitSizeOption) => {
setDataVisualizerListState({
...dataVisualizerListState,
limitSize: newLimitSize,
});
};
const [lastRefresh, setLastRefresh] = useState(0);
const _timeBuckets = useTimeBuckets();
const timefilter = useTimefilter({
timeRangeSelector: true,
autoRefreshSelector: true,
});
const indexPattern = useMemo(() => {
let indexPatternFromQuery = '';
if ('sql' in query) {
indexPatternFromQuery = getIndexPatternFromSQLQuery(query.sql);
}
if ('esql' in query) {
indexPatternFromQuery = getIndexPatternFromESQLQuery(query.esql);
}
// we should find a better way to work with ESQL queries which dont need a dataview
if (indexPatternFromQuery === '') {
return undefined;
}
return indexPatternFromQuery;
}, [query]);
const restorableDefaults = useMemo(
() => getDefaultDataVisualizerListState({}),
() => getDefaultESQLDataVisualizerListState({}),
// We just need to load the saved preference when the page is first loaded
[]
@ -190,25 +84,26 @@ export const IndexDataVisualizerESQL: FC<IndexDataVisualizerESQLProps> = (dataVi
DATA_VISUALIZER_INDEX_VIEWER,
restorableDefaults
);
const [globalState, setGlobalState] = useUrlState('_g');
const showEmptyFields =
dataVisualizerListState.showEmptyFields ?? restorableDefaults.showEmptyFields;
const toggleShowEmptyFields = () => {
setDataVisualizerListState({
...dataVisualizerListState,
showEmptyFields: !dataVisualizerListState.showEmptyFields,
});
const updateDataView = (dv: DataView) => {
if (dv.id !== currentDataView?.id) {
setCurrentDataView(dv);
}
};
const limitSize = dataVisualizerListState.limitSize ?? restorableDefaults.limitSize;
// Query that has been typed, but has not submitted with cmd + enter
const [localQuery, setLocalQuery] = useState<ESQLQuery>({ esql: '' });
const updateLimitSize = (newLimitSize: ESQLDefaultLimitSizeOption) => {
setDataVisualizerListState({
...dataVisualizerListState,
limitSize: newLimitSize,
});
};
const indexPattern = useMemo(() => {
let indexPatternFromQuery = '';
if (isESQLQuery(query)) {
indexPatternFromQuery = getIndexPatternFromESQLQuery(query.esql);
}
// we should find a better way to work with ESQL queries which dont need a dataview
if (indexPatternFromQuery === '') {
return undefined;
}
return indexPatternFromQuery;
}, [query]);
useEffect(
function updateAdhocDataViewFromQuery() {
@ -239,427 +134,19 @@ export const IndexDataVisualizerESQL: FC<IndexDataVisualizerESQLProps> = (dataVi
[indexPattern, data.dataViews, currentDataView]
);
/** Search strategy **/
const fieldStatsRequest = useMemo(() => {
// Obtain the interval to use for date histogram aggregations
// (such as the document count chart). Aim for 75 bars.
const buckets = _timeBuckets;
const tf = timefilter;
if (!buckets || !tf || (isESQLQuery(query) && query.esql === '')) return;
const activeBounds = tf.getActiveBounds();
let earliest: number | undefined;
let latest: number | undefined;
if (activeBounds !== undefined && currentDataView?.timeFieldName !== undefined) {
earliest = activeBounds.min?.valueOf();
latest = activeBounds.max?.valueOf();
}
const bounds = tf.getActiveBounds();
const barTarget = uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET) ?? DEFAULT_BAR_TARGET;
buckets.setInterval('auto');
if (bounds) {
buckets.setBounds(bounds);
buckets.setBarTarget(barTarget);
}
const aggInterval = buckets.getInterval();
const filter = currentDataView?.timeFieldName
? ({
bool: {
must: [],
filter: [
{
range: {
[currentDataView.timeFieldName]: {
format: 'strict_date_optional_time',
gte: timefilter.getTime().from,
lte: timefilter.getTime().to,
},
},
},
],
should: [],
must_not: [],
},
} as QueryDslQueryContainer)
: undefined;
const input: DataVisualizerGridInput<ESQLQuery> = useMemo(() => {
return {
earliest,
latest,
aggInterval,
intervalMs: aggInterval?.asMilliseconds(),
searchQuery: query,
limitSize,
dataView: currentDataView,
query,
savedSearch: undefined,
sessionId: undefined,
visibleFieldNames: undefined,
allowEditDataView: true,
id: 'esql_data_visualizer',
indexPattern,
timeFieldName: currentDataView?.timeFieldName,
runtimeFieldMap: currentDataView?.getRuntimeMappings(),
lastRefresh,
filter,
};
}, [
_timeBuckets,
timefilter,
currentDataView?.id,
JSON.stringify(query),
indexPattern,
lastRefresh,
limitSize,
]);
}, [currentDataView, query?.esql]);
useEffect(() => {
// Force refresh on index pattern change
setLastRefresh(Date.now());
}, [setLastRefresh]);
useEffect(() => {
if (globalState?.time !== undefined) {
timefilter.setTime({
from: globalState.time.from,
to: globalState.time.to,
});
}
}, [JSON.stringify(globalState?.time), timefilter]);
useEffect(() => {
const timeUpdateSubscription = merge(
timefilter.getTimeUpdate$(),
timefilter.getAutoRefreshFetch$(),
mlTimefilterRefresh$
).subscribe(() => {
setGlobalState({
time: timefilter.getTime(),
refreshInterval: timefilter.getRefreshInterval(),
});
setLastRefresh(Date.now());
});
return () => {
timeUpdateSubscription.unsubscribe();
};
}, []);
useEffect(() => {
if (globalState?.refreshInterval !== undefined) {
timefilter.setRefreshInterval(globalState.refreshInterval);
}
}, [JSON.stringify(globalState?.refreshInterval), timefilter]);
const {
documentCountStats,
totalCount,
overallStats,
overallStatsProgress,
columns,
cancelOverallStatsRequest,
} = useESQLOverallStatsData(fieldStatsRequest);
const [metricConfigs, setMetricConfigs] = useState(defaults.metricConfigs);
const [metricsLoaded] = useState(defaults.metricsLoaded);
const [metricsStats, setMetricsStats] = useState<undefined | MetricFieldsStats>();
const [nonMetricConfigs, setNonMetricConfigs] = useState(defaults.nonMetricConfigs);
const [nonMetricsLoaded] = useState(defaults.nonMetricsLoaded);
const [fieldStatFieldsToFetch, setFieldStatFieldsToFetch] = useState<Column[] | undefined>();
const visibleFieldTypes =
dataVisualizerListState.visibleFieldTypes ?? restorableDefaults.visibleFieldTypes;
const visibleFieldNames =
dataVisualizerListState.visibleFieldNames ?? restorableDefaults.visibleFieldNames;
useEffect(
function updateFieldStatFieldsToFetch() {
const { sortField, sortDirection } = dataVisualizerListState;
// Otherwise, sort the list of fields by the initial sort field and sort direction
// Then divide into chunks by the initial page size
const itemsSorter = Comparators.property(
sortField as string,
Comparators.default(sortDirection as 'asc' | 'desc' | undefined)
);
const preslicedSortedConfigs = [...nonMetricConfigs, ...metricConfigs]
.map((c) => ({
...c,
name: c.fieldName,
docCount: c.stats?.count,
cardinality: c.stats?.cardinality,
}))
.sort(itemsSorter);
const filteredItems = filterFields(
preslicedSortedConfigs,
dataVisualizerListState.visibleFieldNames,
dataVisualizerListState.visibleFieldTypes
);
const { pageIndex, pageSize } = dataVisualizerListState;
const pageOfConfigs = filteredItems.filteredFields
?.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize)
.filter((d) => d.existsInDocs === true);
setFieldStatFieldsToFetch(pageOfConfigs);
},
[
dataVisualizerListState.pageIndex,
dataVisualizerListState.pageSize,
dataVisualizerListState.sortField,
dataVisualizerListState.sortDirection,
nonMetricConfigs,
metricConfigs,
]
);
const { fieldStats, fieldStatsProgress, cancelFieldStatsRequest } = useESQLFieldStatsData({
searchQuery: fieldStatsRequest?.searchQuery,
columns: fieldStatFieldsToFetch,
filter: fieldStatsRequest?.filter,
limitSize: fieldStatsRequest?.limitSize,
});
const createMetricCards = useCallback(() => {
if (!columns || !overallStats) return;
const configs: FieldVisConfig[] = [];
const aggregatableExistsFields: AggregatableField[] =
overallStats.aggregatableExistsFields || [];
const allMetricFields = columns.filter((f) => {
return f.secondaryType === KBN_FIELD_TYPES.NUMBER;
});
const metricExistsFields = allMetricFields.filter((f) => {
return aggregatableExistsFields.find((existsF) => {
return existsF.fieldName === f.name;
});
});
let _aggregatableFields: AggregatableField[] = 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.name;
});
if (!fieldData) return;
const metricConfig: FieldVisConfig = {
...field,
...fieldData,
loading: fieldData?.existsInDocs ?? true,
fieldFormat:
currentDataView?.getFormatterForFieldNoDefault(field.name) ??
fieldFormats.deserialize({ id: field.secondaryType }),
aggregatable: true,
deletable: false,
type: getFieldType(field) as SupportedFieldType,
};
configs.push(metricConfig);
});
setMetricsStats({
totalMetricFieldsCount: allMetricFields.length,
visibleMetricsCount: metricFieldsToShow.length,
});
setMetricConfigs(configs);
}, [metricsLoaded, overallStats, showEmptyFields, columns, currentDataView?.id]);
const createNonMetricCards = useCallback(() => {
if (!columns || !overallStats) return;
const allNonMetricFields = columns.filter((f) => {
return f.secondaryType !== KBN_FIELD_TYPES.NUMBER;
});
// Obtain the list of all non-metric fields which appear in documents
// (aggregatable or not aggregatable).
const populatedNonMetricFields: Column[] = []; // Kibana index pattern non metric fields.
let nonMetricFieldData: Array<AggregatableField | NonAggregatableField> = []; // Basic non metric field data loaded from requesting overall stats.
const aggregatableExistsFields: AggregatableField[] =
overallStats.aggregatableExistsFields || [];
const nonAggregatableExistsFields: NonAggregatableField[] =
overallStats.nonAggregatableExistsFields || [];
allNonMetricFields.forEach((f) => {
const checkAggregatableField = aggregatableExistsFields.find(
(existsField) => existsField.fieldName === f.name
);
if (checkAggregatableField !== undefined) {
populatedNonMetricFields.push(f);
nonMetricFieldData.push(checkAggregatableField);
} else {
const checkNonAggregatableField = nonAggregatableExistsFields.find(
(existsField) => existsField.fieldName === f.name
);
if (checkNonAggregatableField !== undefined) {
populatedNonMetricFields.push(f);
nonMetricFieldData.push(checkNonAggregatableField);
}
}
});
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.name);
const nonMetricConfig: Partial<FieldVisConfig> = {
...(fieldData ? fieldData : {}),
secondaryType: getFieldType(field) as SupportedFieldType,
loading: fieldData?.existsInDocs ?? true,
deletable: false,
fieldFormat:
currentDataView?.getFormatterForFieldNoDefault(field.name) ??
fieldFormats.deserialize({ id: field.secondaryType }),
};
// Map the field type from the Kibana index pattern to the field type
// used in the data visualizer.
const dataVisualizerType = getFieldType(field) as SupportedFieldType;
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 as SupportedFieldType;
nonMetricConfig.isUnsupportedType = true;
}
if (field.name !== nonMetricConfig.fieldName) {
nonMetricConfig.displayName = field.name;
}
configs.push(nonMetricConfig as FieldVisConfig);
});
setNonMetricConfigs(configs);
}, [columns, nonMetricsLoaded, overallStats, showEmptyFields, currentDataView?.id]);
const fieldsCountStats: TotalFieldsStats | undefined = useMemo(() => {
if (!overallStats) return;
let _visibleFieldsCount = 0;
let _totalFieldsCount = 0;
Object.keys(overallStats).forEach((key) => {
const fieldsGroup = overallStats[key as keyof typeof overallStats];
if (Array.isArray(fieldsGroup) && fieldsGroup.length > 0) {
_totalFieldsCount += fieldsGroup.length;
}
});
if (showEmptyFields === true) {
_visibleFieldsCount = _totalFieldsCount;
} else {
_visibleFieldsCount =
overallStats.aggregatableExistsFields.length +
overallStats.nonAggregatableExistsFields.length;
}
return { visibleFieldsCount: _visibleFieldsCount, totalFieldsCount: _totalFieldsCount };
}, [overallStats, showEmptyFields]);
useEffect(() => {
createMetricCards();
createNonMetricCards();
}, [overallStats, showEmptyFields]);
const configs = useMemo(() => {
let combinedConfigs = [...nonMetricConfigs, ...metricConfigs];
combinedConfigs = filterFields(
combinedConfigs,
visibleFieldNames,
visibleFieldTypes
).filteredFields;
if (fieldStatsProgress.loaded === 100 && fieldStats) {
combinedConfigs = combinedConfigs.map((c) => {
const loadedFullStats = fieldStats.get(c.fieldName) ?? {};
return loadedFullStats
? {
...c,
loading: false,
stats: { ...c.stats, ...loadedFullStats },
}
: c;
});
}
return combinedConfigs;
}, [
nonMetricConfigs,
metricConfigs,
visibleFieldTypes,
visibleFieldNames,
fieldStatsProgress.loaded,
dataVisualizerListState.pageIndex,
dataVisualizerListState.pageSize,
]);
// 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();
}
};
}, []);
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}
dataView={currentDataView}
combinedQuery={{ searchQueryLanguage: 'kuery', searchString: '' }}
totalDocuments={totalCount}
typeAccessor="secondaryType"
/>
);
}
return m;
}, {} as ItemIdToExpandedRowMap);
},
[currentDataView, totalCount]
);
const hasValidTimeField = useMemo(
() =>
currentDataView &&
currentDataView.timeFieldName !== undefined &&
currentDataView.timeFieldName !== '',
[currentDataView]
);
const isWithinLargeBreakpoint = useIsWithinMaxBreakpoint('l');
const dvPageHeader = css({
[useEuiBreakpoint(['xs', 's', 'm', 'l'])]: {
flexDirection: 'column',
@ -667,43 +154,47 @@ export const IndexDataVisualizerESQL: FC<IndexDataVisualizerESQLProps> = (dataVi
},
});
const combinedProgress = useMemo(
() => overallStatsProgress.loaded * 0.3 + fieldStatsProgress.loaded * 0.7,
[overallStatsProgress.loaded, fieldStatsProgress.loaded]
const isWithinLargeBreakpoint = useIsWithinMaxBreakpoint('l');
const {
totalCount,
progress: combinedProgress,
overallStatsProgress,
configs,
documentCountStats,
metricsStats,
timefilter,
getItemIdToExpandedRowMap,
onQueryUpdate,
limitSize,
showEmptyFields,
fieldsCountStats,
} = useESQLDataVisualizerData(input, dataVisualizerListState, setQuery);
const hasValidTimeField = useMemo(
() => currentDataView?.timeFieldName !== undefined,
[currentDataView?.timeFieldName]
);
// Query that has been typed, but has not submitted with cmd + enter
const [localQuery, setLocalQuery] = useState<AggregateQuery>({ esql: '' });
const onQueryUpdate = async (q?: AggregateQuery) => {
// When user submits a new query
// resets all current requests and other data
if (cancelOverallStatsRequest) {
cancelOverallStatsRequest();
}
if (cancelFieldStatsRequest) {
cancelFieldStatsRequest();
}
// Reset field stats to fetch state
setFieldStatFieldsToFetch(undefined);
setMetricConfigs(defaults.metricConfigs);
setNonMetricConfigs(defaults.nonMetricConfigs);
if (q) {
setQuery(q);
}
};
useEffect(
function resetFieldStatsFieldToFetch() {
// If query returns 0 document, no need to do more work here
if (totalCount === undefined || totalCount === 0) {
setFieldStatFieldsToFetch(undefined);
return;
}
},
[totalCount]
const queryNeedsUpdate = useMemo(
() => (localQuery.esql !== query.esql ? true : undefined),
[localQuery.esql, query.esql]
);
const handleRefresh = useCallback(() => {
// The page is already autoamtically updating when time range is changed
// via the url state
// so we just need to force update if the query is outdated
if (queryNeedsUpdate) {
setQuery(localQuery);
}
}, [queryNeedsUpdate, localQuery.esql]);
const onTextLangQueryChange = useCallback((q: AggregateQuery) => {
if (isESQLQuery(q)) {
setLocalQuery(q);
}
}, []);
return (
<EuiPageTemplate
offset={0}
@ -743,9 +234,11 @@ export const IndexDataVisualizerESQL: FC<IndexDataVisualizerESQLProps> = (dataVi
) : null}
<EuiFlexItem grow={false}>
<DatePickerWrapper
isAutoRefreshOnly={false}
showRefresh={false}
isAutoRefreshOnly={!hasValidTimeField}
showRefresh={!hasValidTimeField}
width="full"
needsUpdate={queryNeedsUpdate}
onRefresh={handleRefresh}
isDisabled={!hasValidTimeField}
/>
</EuiFlexItem>
@ -754,7 +247,7 @@ export const IndexDataVisualizerESQL: FC<IndexDataVisualizerESQLProps> = (dataVi
<EuiSpacer size="m" />
<TextBasedLangEditor
query={localQuery}
onTextLangQueryChange={setLocalQuery}
onTextLangQueryChange={onTextLangQueryChange}
onTextLangQuerySubmit={onQueryUpdate}
expandCodeEditor={() => false}
isCodeEditorExpanded={true}
@ -777,9 +270,9 @@ export const IndexDataVisualizerESQL: FC<IndexDataVisualizerESQLProps> = (dataVi
showSettings={false}
/>
</EuiFlexGroup>
<EuiSpacer size="s" />
</>
)}
<EuiSpacer size="m" />
<EuiFlexGroup direction="row">
<FieldCountPanel
showEmptyFields={showEmptyFields}
@ -793,7 +286,8 @@ export const IndexDataVisualizerESQL: FC<IndexDataVisualizerESQLProps> = (dataVi
onChangeLimitSize={updateLimitSize}
/>
</EuiFlexGroup>
<EuiSpacer size="m" />
<EuiSpacer size="s" />
<EuiProgress value={combinedProgress} max={100} size="xs" />
<DataVisualizerTable<FieldVisConfig>
items={configs}

View file

@ -48,9 +48,9 @@ import {
DataVisualizerTable,
ItemIdToExpandedRowMap,
} from '../../../common/components/stats_table';
import { FieldVisConfig } from '../../../common/components/stats_table/types';
import type { FieldVisConfig } from '../../../common/components/stats_table/types';
import type { TotalFieldsStats } from '../../../common/components/stats_table/components/field_count_stats';
import { OverallStats } from '../../types/overall_stats';
import type { OverallStats } from '../../types/overall_stats';
import { IndexBasedDataVisualizerExpandedRow } from '../../../common/components/expanded_row/index_based_expanded_row';
import { DATA_VISUALIZER_INDEX_VIEWER } from '../../constants/index_data_visualizer_viewer';
import {
@ -66,46 +66,17 @@ import { ActionsPanel } from '../actions_panel';
import { DataVisualizerDataViewManagement } from '../data_view_management';
import type { GetAdditionalLinks } from '../../../common/components/results_links';
import { useDataVisualizerGridData } from '../../hooks/use_data_visualizer_grid_data';
import type { DataVisualizerGridInput } from '../../embeddables/grid_embeddable/grid_embeddable';
import {
MIN_SAMPLER_PROBABILITY,
RANDOM_SAMPLER_OPTION,
RandomSamplerOption,
type RandomSamplerOption,
} from '../../constants/random_sampler';
interface DataVisualizerPageState {
overallStats: OverallStats;
metricConfigs: FieldVisConfig[];
totalMetricFieldCount: number;
populatedMetricFieldCount: number;
metricsLoaded: boolean;
nonMetricConfigs: FieldVisConfig[];
nonMetricsLoaded: boolean;
documentCountStats?: FieldVisConfig;
}
import type { DataVisualizerGridInput } from '../../embeddables/grid_embeddable/types';
const defaultSearchQuery = {
match_all: {},
};
export function getDefaultPageState(): DataVisualizerPageState {
return {
overallStats: {
totalCount: 0,
aggregatableExistsFields: [],
aggregatableNotExistsFields: [],
nonAggregatableExistsFields: [],
nonAggregatableNotExistsFields: [],
},
metricConfigs: [],
totalMetricFieldCount: 0,
populatedMetricFieldCount: 0,
metricsLoaded: false,
nonMetricConfigs: [],
nonMetricsLoaded: false,
documentCountStats: undefined,
};
}
export const getDefaultDataVisualizerListState = (
overrides?: Partial<DataVisualizerIndexBasedAppState>
): Required<DataVisualizerIndexBasedAppState> => ({
@ -244,7 +215,7 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
});
};
const input: DataVisualizerGridInput = useMemo(() => {
const input: Required<DataVisualizerGridInput, 'dataView'> = useMemo(() => {
return {
dataView: currentDataView,
savedSearch: currentSavedSearch,

View file

@ -8,6 +8,7 @@ import React, { type ChangeEvent } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiSelect, EuiText, useGeneratedHtmlId } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import type { ESQLDefaultLimitSizeOption } from '../../../embeddables/grid_embeddable/types';
const options = [
{
@ -46,8 +47,6 @@ const options = [
},
];
export type ESQLDefaultLimitSizeOption = '5000' | '10000' | '100000' | '1000000' | 'none';
export const ESQLDefaultLimitSizeSelect = ({
limitSize,
onChangeLimitSize,
@ -71,7 +70,7 @@ export const ESQLDefaultLimitSizeSelect = ({
defaultMessage: 'Limit size',
})}
prepend={
<EuiText textAlign="center" size="s">
<EuiText textAlign="center">
<FormattedMessage
id="xpack.dataVisualizer.searchPanel.esql.limitSizeLabel"
defaultMessage="Limit analysis to"

View file

@ -5,6 +5,27 @@
* 2.0.
*/
import type { DataVisualizerPageState } from '../types/index_data_visualizer_state';
export const DATA_VISUALIZER_INDEX_VIEWER = 'DATA_VISUALIZER_INDEX_VIEWER';
export const MAX_CONCURRENT_REQUESTS = 10;
export function getDefaultPageState(): DataVisualizerPageState {
return {
overallStats: {
totalCount: 0,
aggregatableExistsFields: [],
aggregatableNotExistsFields: [],
nonAggregatableExistsFields: [],
nonAggregatableNotExistsFields: [],
},
metricConfigs: [],
totalMetricFieldCount: 0,
populatedMetricFieldCount: 0,
metricsLoaded: false,
nonMetricConfigs: [],
nonMetricsLoaded: false,
documentCountStats: undefined,
};
}

View file

@ -0,0 +1,74 @@
/*
* 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, { useCallback, useEffect, useState } from 'react';
import type { EmbeddableInput } from '@kbn/embeddable-plugin/public';
import type { FieldVisConfig } from '../../../../../common/types/field_vis_config';
import type { DataVisualizerTableState } from '../../../../../common/types';
import { DataVisualizerTable } from '../../../common/components/stats_table';
import {
getDefaultESQLDataVisualizerListState,
useESQLDataVisualizerData,
} from '../../hooks/esql/use_data_visualizer_esql_data';
import {
ESQLDataVisualizerGridEmbeddableInput,
ESQLDataVisualizerIndexBasedAppState,
} from './types';
import { EmbeddableNoResultsEmptyPrompt } from './embeddable_field_stats_no_results';
const restorableDefaults = getDefaultESQLDataVisualizerListState();
export const EmbeddableESQLFieldStatsTableWrapper = ({
input,
onOutputChange,
}: {
input: EmbeddableInput & ESQLDataVisualizerGridEmbeddableInput;
onOutputChange?: (ouput: any) => void;
}) => {
const [dataVisualizerListState, setDataVisualizerListState] =
useState<Required<ESQLDataVisualizerIndexBasedAppState>>(restorableDefaults);
const onTableChange = useCallback(
(update: DataVisualizerTableState) => {
setDataVisualizerListState({ ...dataVisualizerListState, ...update });
if (onOutputChange) {
onOutputChange(update);
}
},
[dataVisualizerListState, onOutputChange]
);
const {
configs,
extendedColumns,
progress,
overallStatsProgress,
setLastRefresh,
getItemIdToExpandedRowMap,
} = useESQLDataVisualizerData(input, dataVisualizerListState);
useEffect(() => {
setLastRefresh(Date.now());
}, [input?.lastReloadRequestTime, setLastRefresh]);
if (progress === 100 && configs.length === 0) {
return <EmbeddableNoResultsEmptyPrompt />;
}
return (
<DataVisualizerTable<FieldVisConfig>
items={configs}
pageState={dataVisualizerListState}
updatePageState={onTableChange}
getItemIdToExpandedRowMap={getItemIdToExpandedRowMap}
extendedColumns={extendedColumns}
showPreviewByDefault={input?.showPreviewByDefault}
onChange={onOutputChange}
loading={progress < 100}
overallStatsRunning={overallStatsProgress.isRunning}
/>
);
};

View file

@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { css } from '@emotion/react';
import { EuiIcon, EuiSpacer, EuiText } from '@elastic/eui';
export const EmbeddableNoResultsEmptyPrompt = () => (
<div
css={css({
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>
);

View file

@ -0,0 +1,96 @@
/*
* 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, { useCallback, useEffect, useState } from 'react';
import type { Required } from 'utility-types';
import type { DataVisualizerGridEmbeddableInput } from './types';
import {
DataVisualizerTable,
ItemIdToExpandedRowMap,
} from '../../../common/components/stats_table';
import type { FieldVisConfig } from '../../../common/components/stats_table/types';
import { getDefaultDataVisualizerListState } from '../../components/index_data_visualizer_view/index_data_visualizer_view';
import type { DataVisualizerTableState } from '../../../../../common/types';
import type { DataVisualizerIndexBasedAppState } from '../../types/index_data_visualizer_state';
import { IndexBasedDataVisualizerExpandedRow } from '../../../common/components/expanded_row/index_based_expanded_row';
import { useDataVisualizerGridData } from '../../hooks/use_data_visualizer_grid_data';
import { EmbeddableNoResultsEmptyPrompt } from './embeddable_field_stats_no_results';
const restorableDefaults = getDefaultDataVisualizerListState();
export const EmbeddableFieldStatsTableWrapper = ({
input,
onOutputChange,
}: {
input: Required<DataVisualizerGridEmbeddableInput, 'dataView'>;
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,
progress,
overallStatsProgress,
setLastRefresh,
} = useDataVisualizerGridData(input, dataVisualizerListState);
useEffect(() => {
setLastRefresh(Date.now());
}, [input?.lastReloadRequestTime, setLastRefresh]);
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}
dataView={input.dataView}
combinedQuery={{ searchQueryLanguage, searchString }}
onAddFilter={input.onAddFilter}
totalDocuments={input.totalDocuments}
/>
);
}
return m;
}, {} as ItemIdToExpandedRowMap);
},
[input, searchQueryLanguage, searchString]
);
if (progress === 100 && configs.length === 0) {
return <EmbeddableNoResultsEmptyPrompt />;
}
return (
<DataVisualizerTable<FieldVisConfig>
items={configs}
pageState={dataVisualizerListState}
updatePageState={onTableChange}
getItemIdToExpandedRowMap={getItemIdToExpandedRowMap}
extendedColumns={extendedColumns}
showPreviewByDefault={input?.showPreviewByDefault}
onChange={onOutputChange}
loading={progress < 100}
overallStatsRunning={overallStatsProgress.isRunning}
/>
);
};

View file

@ -9,156 +9,41 @@ import { pick } from 'lodash';
import { Observable, Subject } from 'rxjs';
import { CoreStart } from '@kbn/core/public';
import ReactDOM from 'react-dom';
import React, { Suspense, useCallback, useEffect, useState } from 'react';
import React, { Suspense } from 'react';
import useObservable from 'react-use/lib/useObservable';
import { EuiEmptyPrompt, EuiIcon, EuiSpacer, EuiText } from '@elastic/eui';
import { Filter } from '@kbn/es-query';
import { EuiEmptyPrompt } from '@elastic/eui';
import { Required } from 'utility-types';
import { FormattedMessage } from '@kbn/i18n-react';
import {
Embeddable,
EmbeddableInput,
EmbeddableOutput,
IContainer,
} from '@kbn/embeddable-plugin/public';
import { Embeddable, IContainer } from '@kbn/embeddable-plugin/public';
import { UI_SETTINGS } from '@kbn/data-plugin/common';
import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import type { Query } from '@kbn/es-query';
import { DataView, DataViewField } from '@kbn/data-views-plugin/public';
import { DatePickerContextProvider } from '@kbn/ml-date-picker';
import type { SavedSearch } from '@kbn/saved-search-plugin/public';
import type { SamplingOption } from '../../../../../common/types/field_stats';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import type { DataVisualizerStartDependencies } from '../../../../plugin';
import { DATA_VISUALIZER_GRID_EMBEDDABLE_TYPE } from './constants';
import { EmbeddableLoading } from './embeddable_loading_fallback';
import { DataVisualizerStartDependencies } from '../../../../plugin';
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 type { DataVisualizerTableState } from '../../../../../common/types';
import type { DataVisualizerIndexBasedAppState } from '../../types/index_data_visualizer_state';
import { IndexBasedDataVisualizerExpandedRow } from '../../../common/components/expanded_row/index_based_expanded_row';
import { useDataVisualizerGridData } from '../../hooks/use_data_visualizer_grid_data';
import { EmbeddableESQLFieldStatsTableWrapper } from './embeddable_esql_field_stats_table';
import { EmbeddableFieldStatsTableWrapper } from './embeddable_field_stats_table';
import type {
DataVisualizerGridEmbeddableInput,
ESQLDataVisualizerGridEmbeddableInput,
DataVisualizerGridEmbeddableOutput,
} from './types';
export type DataVisualizerGridEmbeddableServices = [CoreStart, DataVisualizerStartDependencies];
export interface DataVisualizerGridInput {
dataView: DataView;
savedSearch?: SavedSearch | null;
query?: Query;
visibleFieldNames?: string[];
filters?: Filter[];
showPreviewByDefault?: boolean;
allowEditDataView?: boolean;
id?: string;
/**
* Callback to add a filter to filter bar
*/
onAddFilter?: (field: DataViewField | string, value: string, type: '+' | '-') => void;
sessionId?: string;
fieldsToFetch?: string[];
totalDocuments?: number;
samplingOption?: SamplingOption;
}
export type DataVisualizerGridEmbeddableInput = EmbeddableInput & DataVisualizerGridInput;
export type DataVisualizerGridEmbeddableOutput = EmbeddableOutput;
export type IDataVisualizerGridEmbeddable = typeof DataVisualizerGridEmbeddable;
const restorableDefaults = getDefaultDataVisualizerListState();
function isESQLDataVisualizerEmbeddableInput(
input: unknown
): input is ESQLDataVisualizerGridEmbeddableInput {
return isPopulatedObject(input, ['esql']) && input.esql === true;
}
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,
progress,
overallStatsProgress,
setLastRefresh,
} = useDataVisualizerGridData(input, dataVisualizerListState);
useEffect(() => {
setLastRefresh(Date.now());
}, [input?.lastReloadRequestTime, setLastRefresh]);
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}
dataView={input.dataView}
combinedQuery={{ searchQueryLanguage, searchString }}
onAddFilter={input.onAddFilter}
totalDocuments={input.totalDocuments}
/>
);
}
return m;
}, {} as ItemIdToExpandedRowMap);
},
[input, searchQueryLanguage, searchString]
);
if (progress === 100 && configs.length === 0) {
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}
loading={progress < 100}
overallStatsRunning={overallStatsProgress.isRunning}
/>
);
};
function isDataVisualizerEmbeddableInput(
input: unknown
): input is Required<DataVisualizerGridEmbeddableInput, 'dataView'> {
return isPopulatedObject(input, ['dataView']);
}
export const IndexDataVisualizerViewWrapper = (props: {
id: string;
@ -169,8 +54,12 @@ export const IndexDataVisualizerViewWrapper = (props: {
const { embeddableInput, onOutputChange } = props;
const input = useObservable(embeddableInput);
if (input && input.dataView) {
return <EmbeddableWrapper input={input} onOutputChange={onOutputChange} />;
if (isESQLDataVisualizerEmbeddableInput(input)) {
return <EmbeddableESQLFieldStatsTableWrapper input={input} onOutputChange={onOutputChange} />;
}
if (isDataVisualizerEmbeddableInput(input)) {
return <EmbeddableFieldStatsTableWrapper input={input} onOutputChange={onOutputChange} />;
} else {
return (
<EuiEmptyPrompt

View file

@ -9,11 +9,9 @@ import { i18n } from '@kbn/i18n';
import { StartServicesAccessor } from '@kbn/core/public';
import { EmbeddableFactoryDefinition, IContainer } from '@kbn/embeddable-plugin/public';
import { DATA_VISUALIZER_GRID_EMBEDDABLE_TYPE } from './constants';
import {
DataVisualizerGridEmbeddableInput,
DataVisualizerGridEmbeddableServices,
} from './grid_embeddable';
import { DataVisualizerGridEmbeddableServices } from './grid_embeddable';
import { DataVisualizerPluginStart, DataVisualizerStartDependencies } from '../../../../plugin';
import { DataVisualizerGridEmbeddableInput } from './types';
export class DataVisualizerGridEmbeddableFactory
implements EmbeddableFactoryDefinition<DataVisualizerGridEmbeddableInput>

View file

@ -0,0 +1,59 @@
/*
* 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 { Filter } from '@kbn/es-query';
import type { EmbeddableInput, EmbeddableOutput } from '@kbn/embeddable-plugin/public';
import type { Query } from '@kbn/es-query';
import { DataView, DataViewField } from '@kbn/data-views-plugin/public';
import type { SavedSearch } from '@kbn/saved-search-plugin/public';
import type { SamplingOption } from '../../../../../common/types/field_stats';
import { DATA_VISUALIZER_INDEX_VIEWER } from '../../constants/index_data_visualizer_viewer';
import { DataVisualizerIndexBasedAppState } from '../../types/index_data_visualizer_state';
import type { ESQLQuery } from '../../search_strategy/requests/esql_utils';
export interface DataVisualizerGridInput<T = Query> {
dataView?: DataView;
savedSearch?: SavedSearch | null;
query?: T;
visibleFieldNames?: string[];
filters?: Filter[];
showPreviewByDefault?: boolean;
allowEditDataView?: boolean;
id?: string;
/**
* Callback to add a filter to filter bar
*/
onAddFilter?: (field: DataViewField | string, value: string, type: '+' | '-') => void;
sessionId?: string;
fieldsToFetch?: string[];
totalDocuments?: number;
samplingOption?: SamplingOption;
/**
* If esql:true, switch table to ES|QL mode
*/
esql?: boolean;
/**
* If esql:true, the index pattern is used to validate time field
*/
indexPattern?: string;
}
export type ESQLDataVisualizerGridEmbeddableInput = DataVisualizerGridInput<ESQLQuery>;
export type DataVisualizerGridEmbeddableInput = EmbeddableInput & DataVisualizerGridInput;
export type DataVisualizerGridEmbeddableOutput = EmbeddableOutput;
export type ESQLDefaultLimitSizeOption = '5000' | '10000' | '100000' | '1000000' | 'none';
export interface ESQLDataVisualizerIndexBasedAppState extends DataVisualizerIndexBasedAppState {
limitSize: ESQLDefaultLimitSizeOption;
}
export interface ESQLDataVisualizerIndexBasedPageUrlState {
pageKey: typeof DATA_VISUALIZER_INDEX_VIEWER;
pageUrlState: Required<ESQLDataVisualizerIndexBasedAppState>;
}

View file

@ -0,0 +1,628 @@
/*
* 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, { useEffect, useMemo, useState, useCallback } from 'react';
import { mlTimefilterRefresh$, useTimefilter } from '@kbn/ml-date-picker';
import { merge } from 'rxjs';
import { Comparators } from '@elastic/eui';
import { useUrlState } from '@kbn/ml-url-state';
import { KBN_FIELD_TYPES } from '@kbn/field-types';
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { getFieldType } from '@kbn/field-utils';
import { UI_SETTINGS } from '@kbn/data-service';
import useObservable from 'react-use/lib/useObservable';
import { SEARCH_QUERY_LANGUAGE } from '@kbn/ml-query-utils';
import { KibanaExecutionContext } from '@kbn/core-execution-context-common';
import { useExecutionContext } from '@kbn/kibana-react-plugin/public';
import type { AggregateQuery } from '@kbn/es-query';
import type { SamplingOption } from '../../../../../common/types/field_stats';
import type { FieldVisConfig } from '../../../../../common/types/field_vis_config';
import type { SupportedFieldType } from '../../../../../common/types/job_field_type';
import { useTimeBuckets } from '../../../common/hooks/use_time_buckets';
import { ItemIdToExpandedRowMap } from '../../../common/components/stats_table';
import type {
MetricFieldsStats,
TotalFieldsStats,
} from '../../../common/components/stats_table/components/field_count_stats';
import { filterFields } from '../../../common/components/fields_stats_grid/filter_fields';
import { IndexBasedDataVisualizerExpandedRow } from '../../../common/components/expanded_row/index_based_expanded_row';
import { useESQLFieldStatsData } from './use_esql_field_stats_data';
import type { NonAggregatableField } from '../../types/overall_stats';
import { ESQLQuery, isESQLQuery } from '../../search_strategy/requests/esql_utils';
import { DEFAULT_BAR_TARGET } from '../../../common/constants';
import { type Column, useESQLOverallStatsData } from './use_esql_overall_stats_data';
import { type AggregatableField } from '../../types/esql_data_visualizer';
import { useDataVisualizerKibana } from '../../../kibana_context';
import { DATA_VISUALIZER_GRID_EMBEDDABLE_TYPE } from '../../embeddables/grid_embeddable/constants';
import type {
ESQLDataVisualizerGridEmbeddableInput,
ESQLDataVisualizerIndexBasedAppState,
} from '../../embeddables/grid_embeddable/types';
import { getDefaultPageState } from '../../constants/index_data_visualizer_viewer';
const defaultSearchQuery = {
match_all: {},
};
const FALLBACK_ESQL_QUERY: ESQLQuery = { esql: '' };
const DEFAULT_SAMPLING_OPTION: SamplingOption = {
mode: 'random_sampling',
seed: '',
probability: 0,
};
const DEFAULT_LIMIT_SIZE = '10000';
const defaults = getDefaultPageState();
export const getDefaultESQLDataVisualizerListState = (
overrides?: Partial<ESQLDataVisualizerIndexBasedAppState>
): Required<ESQLDataVisualizerIndexBasedAppState> => ({
pageIndex: 0,
pageSize: 25,
sortField: 'fieldName',
sortDirection: 'asc',
visibleFieldTypes: [],
visibleFieldNames: [],
limitSize: DEFAULT_LIMIT_SIZE,
searchString: '',
searchQuery: defaultSearchQuery,
searchQueryLanguage: SEARCH_QUERY_LANGUAGE.KUERY,
filters: [],
showDistributions: true,
showAllFields: false,
showEmptyFields: false,
probability: null,
rndSamplerPref: 'off',
...overrides,
});
export const useESQLDataVisualizerData = (
input: ESQLDataVisualizerGridEmbeddableInput,
dataVisualizerListState: ESQLDataVisualizerIndexBasedAppState,
setQuery?: React.Dispatch<React.SetStateAction<ESQLQuery>>
) => {
const [lastRefresh, setLastRefresh] = useState(0);
const { services } = useDataVisualizerKibana();
const { uiSettings, fieldFormats, executionContext } = services;
const parentExecutionContext = useObservable(executionContext?.context$);
const embeddableExecutionContext: KibanaExecutionContext = useMemo(() => {
const child: KibanaExecutionContext = {
type: 'visualization',
name: DATA_VISUALIZER_GRID_EMBEDDABLE_TYPE,
id: input.id,
};
return {
...(parentExecutionContext ? parentExecutionContext : {}),
child,
};
}, [parentExecutionContext, input.id]);
useExecutionContext(executionContext, embeddableExecutionContext);
const _timeBuckets = useTimeBuckets();
const timefilter = useTimefilter({
timeRangeSelector: true,
autoRefreshSelector: true,
});
const { currentDataView, query, visibleFieldNames, indexPattern } = useMemo(
() => ({
currentSavedSearch: input?.savedSearch,
currentDataView: input.dataView,
query: input?.query ?? FALLBACK_ESQL_QUERY,
visibleFieldNames: input?.visibleFieldNames ?? [],
currentFilters: input?.filters,
fieldsToFetch: input?.fieldsToFetch,
/** By default, use random sampling **/
samplingOption: input?.samplingOption ?? DEFAULT_SAMPLING_OPTION,
indexPattern: input?.indexPattern,
}),
[input]
);
const restorableDefaults = useMemo(
() => getDefaultESQLDataVisualizerListState(dataVisualizerListState),
// We just need to load the saved preference when the page is first loaded
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
const [globalState, setGlobalState] = useUrlState('_g');
const showEmptyFields =
dataVisualizerListState.showEmptyFields ?? restorableDefaults.showEmptyFields;
const limitSize = dataVisualizerListState.limitSize ?? restorableDefaults.limitSize;
/** Search strategy **/
const fieldStatsRequest = useMemo(
() => {
// Obtain the interval to use for date histogram aggregations
// (such as the document count chart). Aim for 75 bars.
const buckets = _timeBuckets;
const tf = timefilter;
if (!buckets || !tf || (isESQLQuery(query) && query.esql === '')) return;
const activeBounds = tf.getActiveBounds();
let earliest: number | undefined;
let latest: number | undefined;
if (activeBounds !== undefined && currentDataView?.timeFieldName !== undefined) {
earliest = activeBounds.min?.valueOf();
latest = activeBounds.max?.valueOf();
}
const bounds = tf.getActiveBounds();
const barTarget = uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET) ?? DEFAULT_BAR_TARGET;
buckets.setInterval('auto');
if (bounds) {
buckets.setBounds(bounds);
buckets.setBarTarget(barTarget);
}
const aggInterval = buckets.getInterval();
const filter = currentDataView?.timeFieldName
? ({
bool: {
must: [],
filter: [
{
range: {
[currentDataView.timeFieldName]: {
format: 'strict_date_optional_time',
gte: timefilter.getTime().from,
lte: timefilter.getTime().to,
},
},
},
],
should: [],
must_not: [],
},
} as QueryDslQueryContainer)
: undefined;
return {
earliest,
latest,
aggInterval,
intervalMs: aggInterval?.asMilliseconds(),
searchQuery: query,
limitSize,
sessionId: undefined,
indexPattern,
timeFieldName: currentDataView?.timeFieldName,
runtimeFieldMap: currentDataView?.getRuntimeMappings(),
lastRefresh,
filter,
};
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[
_timeBuckets,
timefilter,
currentDataView?.id,
// eslint-disable-next-line react-hooks/exhaustive-deps
JSON.stringify(query),
indexPattern,
lastRefresh,
limitSize,
]
);
useEffect(() => {
// Force refresh on index pattern change
setLastRefresh(Date.now());
}, [setLastRefresh]);
useEffect(
() => {
if (globalState?.time !== undefined) {
timefilter.setTime({
from: globalState.time.from,
to: globalState.time.to,
});
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[JSON.stringify(globalState?.time), timefilter]
);
useEffect(
() => {
const timeUpdateSubscription = merge(
timefilter.getTimeUpdate$(),
timefilter.getAutoRefreshFetch$(),
mlTimefilterRefresh$
).subscribe(() => {
setGlobalState({
time: timefilter.getTime(),
refreshInterval: timefilter.getRefreshInterval(),
});
setLastRefresh(Date.now());
});
return () => {
timeUpdateSubscription.unsubscribe();
};
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
useEffect(
() => {
if (globalState?.refreshInterval !== undefined) {
timefilter.setRefreshInterval(globalState.refreshInterval);
}
}, // eslint-disable-next-line react-hooks/exhaustive-deps
[JSON.stringify(globalState?.refreshInterval), timefilter]
);
const {
documentCountStats,
totalCount,
overallStats,
overallStatsProgress,
columns,
cancelOverallStatsRequest,
timeFieldName,
} = useESQLOverallStatsData(fieldStatsRequest);
const [metricConfigs, setMetricConfigs] = useState(defaults.metricConfigs);
const [metricsLoaded] = useState(defaults.metricsLoaded);
const [metricsStats, setMetricsStats] = useState<undefined | MetricFieldsStats>();
const [nonMetricConfigs, setNonMetricConfigs] = useState(defaults.nonMetricConfigs);
const [nonMetricsLoaded] = useState(defaults.nonMetricsLoaded);
const [fieldStatFieldsToFetch, setFieldStatFieldsToFetch] = useState<Column[] | undefined>();
const visibleFieldTypes =
dataVisualizerListState.visibleFieldTypes ?? restorableDefaults.visibleFieldTypes;
useEffect(
function updateFieldStatFieldsToFetch() {
const { sortField, sortDirection } = dataVisualizerListState;
// Otherwise, sort the list of fields by the initial sort field and sort direction
// Then divide into chunks by the initial page size
const itemsSorter = Comparators.property(
sortField as string,
Comparators.default(sortDirection as 'asc' | 'desc' | undefined)
);
const preslicedSortedConfigs = [...nonMetricConfigs, ...metricConfigs]
.map((c) => ({
...c,
name: c.fieldName,
docCount: c.stats?.count,
cardinality: c.stats?.cardinality,
}))
.sort(itemsSorter);
const filteredItems = filterFields(
preslicedSortedConfigs,
dataVisualizerListState.visibleFieldNames,
dataVisualizerListState.visibleFieldTypes
);
const { pageIndex, pageSize } = dataVisualizerListState;
const pageOfConfigs = filteredItems.filteredFields
?.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize)
.filter((d) => d.existsInDocs === true);
setFieldStatFieldsToFetch(pageOfConfigs);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[
dataVisualizerListState.pageIndex,
dataVisualizerListState.pageSize,
dataVisualizerListState.sortField,
dataVisualizerListState.sortDirection,
nonMetricConfigs,
metricConfigs,
]
);
const { fieldStats, fieldStatsProgress, cancelFieldStatsRequest } = useESQLFieldStatsData({
searchQuery: fieldStatsRequest?.searchQuery,
columns: fieldStatFieldsToFetch,
filter: fieldStatsRequest?.filter,
limitSize: fieldStatsRequest?.limitSize,
});
useEffect(
function resetFieldStatsFieldToFetch() {
// If query returns 0 document, no need to do more work here
if (totalCount === undefined) {
setFieldStatFieldsToFetch(undefined);
}
if (totalCount === 0) {
setMetricConfigs(defaults.metricConfigs);
setNonMetricConfigs(defaults.nonMetricConfigs);
setMetricsStats(undefined);
setFieldStatFieldsToFetch(undefined);
}
},
[totalCount]
);
const createMetricCards = useCallback(
() => {
if (!columns || !overallStats) return;
const configs: FieldVisConfig[] = [];
const aggregatableExistsFields: AggregatableField[] =
overallStats.aggregatableExistsFields || [];
const allMetricFields = columns.filter((f) => {
return f.secondaryType === KBN_FIELD_TYPES.NUMBER;
});
const metricExistsFields = allMetricFields.filter((f) => {
return aggregatableExistsFields.find((existsF) => {
return existsF.fieldName === f.name;
});
});
let _aggregatableFields: AggregatableField[] = 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.name;
});
if (!fieldData) return;
const metricConfig: FieldVisConfig = {
...field,
...fieldData,
loading: fieldData?.existsInDocs ?? true,
fieldFormat: fieldFormats.deserialize({ id: field.secondaryType }),
aggregatable: true,
deletable: false,
type: getFieldType(field) as SupportedFieldType,
};
configs.push(metricConfig);
});
setMetricsStats({
totalMetricFieldsCount: allMetricFields.length,
visibleMetricsCount: metricFieldsToShow.length,
});
setMetricConfigs(configs);
}, // eslint-disable-next-line react-hooks/exhaustive-deps
[metricsLoaded, overallStats, showEmptyFields, columns, currentDataView?.id]
);
const createNonMetricCards = useCallback(
() => {
if (!columns || !overallStats) return;
const allNonMetricFields = columns.filter((f) => {
return f.secondaryType !== KBN_FIELD_TYPES.NUMBER;
});
// Obtain the list of all non-metric fields which appear in documents
// (aggregatable or not aggregatable).
const populatedNonMetricFields: Column[] = []; // Kibana index pattern non metric fields.
let nonMetricFieldData: Array<AggregatableField | NonAggregatableField> = []; // Basic non metric field data loaded from requesting overall stats.
const aggregatableExistsFields: AggregatableField[] =
overallStats.aggregatableExistsFields || [];
const nonAggregatableExistsFields: NonAggregatableField[] =
overallStats.nonAggregatableExistsFields || [];
allNonMetricFields.forEach((f) => {
const checkAggregatableField = aggregatableExistsFields.find(
(existsField) => existsField.fieldName === f.name
);
if (checkAggregatableField !== undefined) {
populatedNonMetricFields.push(f);
nonMetricFieldData.push(checkAggregatableField);
} else {
const checkNonAggregatableField = nonAggregatableExistsFields.find(
(existsField) => existsField.fieldName === f.name
);
if (checkNonAggregatableField !== undefined) {
populatedNonMetricFields.push(f);
nonMetricFieldData.push(checkNonAggregatableField);
}
}
});
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.name);
const nonMetricConfig: Partial<FieldVisConfig> = {
...(fieldData ? fieldData : {}),
secondaryType: getFieldType(field) as SupportedFieldType,
loading: fieldData?.existsInDocs ?? true,
deletable: false,
fieldFormat: fieldFormats.deserialize({ id: field.secondaryType }),
};
// Map the field type from the Kibana index pattern to the field type
// used in the data visualizer.
const dataVisualizerType = getFieldType(field) as SupportedFieldType;
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 as SupportedFieldType;
nonMetricConfig.isUnsupportedType = true;
}
if (field.name !== nonMetricConfig.fieldName) {
nonMetricConfig.displayName = field.name;
}
configs.push(nonMetricConfig as FieldVisConfig);
});
setNonMetricConfigs(configs);
}, // eslint-disable-next-line react-hooks/exhaustive-deps
[columns, nonMetricsLoaded, overallStats, showEmptyFields]
);
const fieldsCountStats: TotalFieldsStats | undefined = useMemo(() => {
if (!overallStats) return;
let _visibleFieldsCount = 0;
let _totalFieldsCount = 0;
Object.keys(overallStats).forEach((key) => {
const fieldsGroup = overallStats[key as keyof typeof overallStats];
if (Array.isArray(fieldsGroup) && fieldsGroup.length > 0) {
_totalFieldsCount += fieldsGroup.length;
}
});
if (showEmptyFields === true) {
_visibleFieldsCount = _totalFieldsCount;
} else {
_visibleFieldsCount =
overallStats.aggregatableExistsFields.length +
overallStats.nonAggregatableExistsFields.length;
}
return { visibleFieldsCount: _visibleFieldsCount, totalFieldsCount: _totalFieldsCount };
}, [overallStats, showEmptyFields]);
useEffect(
() => {
createMetricCards();
createNonMetricCards();
}, // eslint-disable-next-line react-hooks/exhaustive-deps
[overallStats, showEmptyFields]
);
const configs = useMemo(
() => {
let combinedConfigs = [...nonMetricConfigs, ...metricConfigs];
combinedConfigs = filterFields(
combinedConfigs,
visibleFieldNames,
visibleFieldTypes
).filteredFields;
if (fieldStatsProgress.loaded === 100 && fieldStats) {
combinedConfigs = combinedConfigs.map((c) => {
const loadedFullStats = fieldStats.get(c.fieldName) ?? {};
return loadedFullStats
? {
...c,
loading: false,
stats: { ...c.stats, ...loadedFullStats },
}
: c;
});
}
return combinedConfigs;
}, // eslint-disable-next-line react-hooks/exhaustive-deps
[
nonMetricConfigs,
metricConfigs,
visibleFieldTypes,
visibleFieldNames,
fieldStatsProgress.loaded,
dataVisualizerListState.pageIndex,
dataVisualizerListState.pageSize,
]
);
const getItemIdToExpandedRowMap = useCallback(
function (itemIds: string[], items: FieldVisConfig[]): ItemIdToExpandedRowMap {
return itemIds.reduce((map: ItemIdToExpandedRowMap, fieldName: string) => {
const item = items.find((fieldVisConfig) => fieldVisConfig.fieldName === fieldName);
if (item !== undefined) {
map[fieldName] = (
<IndexBasedDataVisualizerExpandedRow
item={item}
dataView={currentDataView}
esql={query.esql}
totalDocuments={totalCount}
typeAccessor="secondaryType"
/>
);
}
return map;
}, {} as ItemIdToExpandedRowMap);
},
[currentDataView, totalCount, query.esql]
);
const combinedProgress = useMemo(
() =>
totalCount === 0
? overallStatsProgress.loaded
: overallStatsProgress.loaded * 0.3 + fieldStatsProgress.loaded * 0.7,
[totalCount, overallStatsProgress.loaded, fieldStatsProgress.loaded]
);
const onQueryUpdate = async (q?: AggregateQuery) => {
// When user submits a new query
// resets all current requests and other data
if (cancelOverallStatsRequest) {
cancelOverallStatsRequest();
}
if (cancelFieldStatsRequest) {
cancelFieldStatsRequest();
}
// Reset field stats to fetch state
setFieldStatFieldsToFetch(undefined);
setMetricConfigs(defaults.metricConfigs);
setNonMetricConfigs(defaults.nonMetricConfigs);
if (isESQLQuery(q) && setQuery) {
setQuery(q);
}
};
return {
totalCount,
progress: combinedProgress,
overallStatsProgress,
configs,
// Column with action to lens, data view editor, etc
// set to nothing for now
extendedColumns: undefined,
documentCountStats,
metricsStats,
overallStats,
timefilter,
setLastRefresh,
getItemIdToExpandedRowMap,
cancelOverallStatsRequest,
cancelFieldStatsRequest,
onQueryUpdate,
limitSize,
showEmptyFields,
fieldsCountStats,
timeFieldName,
};
};

View file

@ -20,7 +20,7 @@ import { getESQLNumericFieldStats } from '../../search_strategy/esql_requests/ge
import { getESQLKeywordFieldStats } from '../../search_strategy/esql_requests/get_keyword_fields';
import { getESQLDateFieldStats } from '../../search_strategy/esql_requests/get_date_field_stats';
import { getESQLBooleanFieldStats } from '../../search_strategy/esql_requests/get_boolean_field_stats';
import { getESQLTextFieldStats } from '../../search_strategy/esql_requests/get_text_field_stats';
import { getESQLExampleFieldValues } from '../../search_strategy/esql_requests/get_text_field_stats';
export const useESQLFieldStatsData = <T extends Column>({
searchQuery,
@ -56,13 +56,13 @@ export const useESQLFieldStatsData = <T extends Column>({
const fetchFieldStats = async () => {
cancelRequest();
if (!isESQLQuery(searchQuery) || !allColumns) return;
setFetchState({
...getInitialProgress(),
isRunning: true,
error: undefined,
});
if (!isESQLQuery(searchQuery) || !allColumns) return;
try {
// By default, limit the source data to 100,000 rows
const esqlBaseQuery = searchQuery.esql + getSafeESQLLimitSize(limitSize);
@ -114,8 +114,13 @@ export const useESQLFieldStatsData = <T extends Column>({
}).then(addToProcessedFieldStats);
// GETTING STATS FOR TEXT FIELDS
await getESQLTextFieldStats({
columns: columns.filter((f) => f.secondaryType === 'text'),
await getESQLExampleFieldValues({
columns: columns.filter(
(f) =>
f.secondaryType === 'text' ||
f.secondaryType === 'geo_point' ||
f.secondaryType === 'geo_shape'
),
filter,
runRequest,
esqlBaseQuery,

View file

@ -9,7 +9,7 @@ import { ESQL_SEARCH_STRATEGY, KBN_FIELD_TYPES } from '@kbn/data-plugin/common';
import type { QueryDslQueryContainer } from '@kbn/data-views-plugin/common/types';
import type { AggregateQuery } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import { useCallback, useEffect, useMemo, useReducer } from 'react';
import { useCallback, useEffect, useMemo, useReducer, useRef } from 'react';
import { type UseCancellableSearch, useCancellableSearch } from '@kbn/ml-cancellable-search';
import type { estypes } from '@elastic/elasticsearch';
import type { ISearchOptions } from '@kbn/data-plugin/common';
@ -29,13 +29,13 @@ import {
} from '../../search_strategy/requests/esql_utils';
import type { NonAggregatableField } from '../../types/overall_stats';
import { getESQLSupportedAggs } from '../../utils/get_supported_aggs';
import type { ESQLDefaultLimitSizeOption } from '../../components/search_panel/esql/limit_size';
import { getESQLOverallStats } from '../../search_strategy/esql_requests/get_count_and_cardinality';
import type { AggregatableField } from '../../types/esql_data_visualizer';
import {
handleError,
type HandleErrorCallback,
} from '../../search_strategy/esql_requests/handle_error';
import type { ESQLDefaultLimitSizeOption } from '../../embeddables/grid_embeddable/types';
export interface Column {
type: string;
@ -66,7 +66,7 @@ const getESQLDocumentCountStats = async (
intervalMs?: number,
searchOptions?: ISearchOptions,
onError?: HandleErrorCallback
): Promise<{ documentCountStats?: DocumentCountStats; totalCount: number }> => {
): Promise<{ documentCountStats?: DocumentCountStats; totalCount: number; request?: object }> => {
if (!isESQLQuery(query)) {
throw Error(
i18n.translate('xpack.dataVisualizer.esql.noQueryProvided', {
@ -116,7 +116,7 @@ const getESQLDocumentCountStats = async (
buckets: _buckets,
totalCount,
};
return { documentCountStats: result, totalCount };
return { documentCountStats: result, totalCount, request };
} catch (error) {
handleError({
request,
@ -139,6 +139,7 @@ const getESQLDocumentCountStats = async (
try {
const esqlResults = await runRequest(request, { ...(searchOptions ?? {}), strategy: 'esql' });
return {
request,
documentCountStats: undefined,
totalCount: esqlResults?.rawResponse.values[0][0],
};
@ -188,6 +189,7 @@ export const useESQLOverallStatsData = (
lastRefresh: number;
filter?: QueryDslQueryContainer;
limitSize?: ESQLDefaultLimitSizeOption;
totalCount?: number;
}
| undefined
) => {
@ -198,6 +200,7 @@ export const useESQLOverallStatsData = (
},
} = useDataVisualizerKibana();
const previousDocCountRequest = useRef('');
const { runRequest, cancelRequest } = useCancellableSearch(data);
const [tableData, setTableData] = useReducer(getReducer<Data>(), getInitialData());
@ -226,9 +229,14 @@ export const useESQLOverallStatsData = (
isRunning: true,
error: undefined,
});
setTableData({ totalCount: undefined, documentCountStats: undefined });
const { searchQuery, intervalMs, filter, limitSize } = fieldStatsRequest;
const {
searchQuery,
intervalMs,
filter: filter,
limitSize,
totalCount: knownTotalCount,
} = fieldStatsRequest;
if (!isESQLQuery(searchQuery)) {
return;
@ -276,17 +284,45 @@ export const useESQLOverallStatsData = (
setTableData({ columns, timeFieldName });
const { totalCount, documentCountStats } = await getESQLDocumentCountStats(
runRequest,
// We don't need to fetch the doc count stats again if only the limit size is changed
// so return the previous totalCount, documentCountStats if available
const hashedDocCountParams = JSON.stringify({
searchQuery,
filter,
timeFieldName,
intervalInMs,
undefined,
onError
);
});
let { totalCount, documentCountStats } = tableData;
if (knownTotalCount !== undefined) {
totalCount = knownTotalCount;
}
if (
knownTotalCount === undefined &&
(totalCount === undefined ||
documentCountStats === undefined ||
hashedDocCountParams !== previousDocCountRequest.current)
) {
setTableData({ totalCount: undefined, documentCountStats: undefined });
setTableData({ totalCount, documentCountStats });
previousDocCountRequest.current = hashedDocCountParams;
const results = await getESQLDocumentCountStats(
runRequest,
searchQuery,
filter,
timeFieldName,
intervalInMs,
undefined,
onError
);
totalCount = results.totalCount;
documentCountStats = results.documentCountStats;
setTableData({ totalCount, documentCountStats });
}
if (totalCount === undefined) {
totalCount = 0;
}
setOverallStatsProgress({
loaded: 50,
});
@ -342,6 +378,14 @@ export const useESQLOverallStatsData = (
const esqlBaseQueryWithLimit = searchQuery.esql + getSafeESQLLimitSize(limitSize);
if (totalCount === 0) {
setTableData({
aggregatableFields: undefined,
nonAggregatableFields: undefined,
overallStats: undefined,
columns: undefined,
timeFieldName: undefined,
});
setOverallStatsProgress({
loaded: 100,
isRunning: false,

View file

@ -37,14 +37,14 @@ import {
import type { FieldRequestConfig, SupportedFieldType } from '../../../../common/types';
import { kbnTypeToSupportedType } from '../../common/util/field_types_utils';
import { getActions } from '../../common/components/field_data_row/action_menu';
import type { DataVisualizerGridInput } from '../embeddables/grid_embeddable/grid_embeddable';
import { getDefaultPageState } from '../components/index_data_visualizer_view/index_data_visualizer_view';
import { useFieldStatsSearchStrategy } from './use_field_stats';
import { useOverallStats } from './use_overall_stats';
import type { OverallStatsSearchStrategyParams } from '../../../../common/types/field_stats';
import type { AggregatableField, NonAggregatableField } from '../types/overall_stats';
import { getSupportedAggs } from '../utils/get_supported_aggs';
import { DEFAULT_BAR_TARGET } from '../../common/constants';
import { DataVisualizerGridInput } from '../embeddables/grid_embeddable/types';
import { getDefaultPageState } from '../constants/index_data_visualizer_viewer';
const defaults = getDefaultPageState();
@ -58,7 +58,8 @@ const DEFAULT_SAMPLING_OPTION: SamplingOption = {
probability: 0,
};
export const useDataVisualizerGridData = (
input: DataVisualizerGridInput,
// Data view is required for non-ES|QL queries like kuery or lucene
input: Required<DataVisualizerGridInput, 'dataView'>,
dataVisualizerListState: Required<DataVisualizerIndexBasedAppState>,
savedRandomSamplerPreference?: RandomSamplerOption,
onUpdate?: (params: Dictionary<unknown>) => void
@ -571,6 +572,7 @@ export const useDataVisualizerGridData = (
// Inject custom action column for the index based visualizer
// Hide the column completely if no access to any of the plugins
const extendedColumns = useMemo(() => {
if (!input.dataView) return undefined;
const actions = getActions(
input.dataView,
services,

View file

@ -30,7 +30,6 @@ import {
processNonAggregatableFieldsExistResponse,
} from '../search_strategy/requests/overall_stats';
import type { OverallStats } from '../types/overall_stats';
import { getDefaultPageState } from '../components/index_data_visualizer_view/index_data_visualizer_view';
import {
DataStatsFetchProgress,
isRandomSamplingOption,
@ -38,7 +37,10 @@ import {
} from '../../../../common/types/field_stats';
import { getDocumentCountStats } from '../search_strategy/requests/get_document_stats';
import { getInitialProgress, getReducer } from '../progress_utils';
import { MAX_CONCURRENT_REQUESTS } from '../constants/index_data_visualizer_viewer';
import {
getDefaultPageState,
MAX_CONCURRENT_REQUESTS,
} from '../constants/index_data_visualizer_viewer';
import { displayError } from '../../common/util/display_error';
/**

View file

@ -76,7 +76,7 @@ export const getESQLBooleanFieldStats = async ({
trueCount = row[0];
}
return {
key_as_string: row[1]?.toString(),
key_as_string: row[1] === false ? 'false' : 'true',
doc_count: row[0],
percent: row[0] / topValuesSampleSize,
};

View file

@ -16,10 +16,10 @@ import { getSafeESQLName } from '../requests/esql_utils';
import { MAX_CONCURRENT_REQUESTS } from '../../constants/index_data_visualizer_viewer';
import type { NonAggregatableField } from '../../types/overall_stats';
import { isFulfilled } from '../../../common/util/promise_all_settled_utils';
import type { ESQLDefaultLimitSizeOption } from '../../components/search_panel/esql/limit_size';
import type { Column } from '../../hooks/esql/use_esql_overall_stats_data';
import { AggregatableField } from '../../types/esql_data_visualizer';
import { handleError, HandleErrorCallback } from './handle_error';
import type { ESQLDefaultLimitSizeOption } from '../../embeddables/grid_embeddable/types';
interface Field extends Column {
aggregatable?: boolean;
@ -42,25 +42,72 @@ const getESQLOverallStatsInChunk = async ({
onError?: HandleErrorCallback;
}) => {
if (fields.length > 0) {
const aggregatableFieldsToQuery = fields.filter((f) => f.aggregatable);
const aggToIndex = { count: 0, cardinality: 1 };
// Track what's the starting index for the next field
// For aggregatable field, we are getting count(EVAL MV_MIN()) and count_disticnt
// For non-aggregatable field, we are getting only count()
let startIndex = 0;
/** Example query:
* from {indexPattern} | LIMIT {limitSize}
* | EVAL `ne_{aggregableField}` = MV_MIN({aggregableField}),
* | STATs `{aggregableField}_count` = COUNT(`ne_{aggregableField}`),
* `{aggregableField}_cardinality` = COUNT_DISTINCT({aggregableField}),
* `{nonAggregableField}_count` = COUNT({nonAggregableField})
*/
const fieldsToFetch: Array<Field & { evalQuery?: string; query: string; startIndex: number }> =
fields.map((field) => {
if (field.aggregatable) {
const result = {
...field,
startIndex,
// Field values can be an array of values (fieldName = ['a', 'b', 'c'])
// and count(fieldName) will count all the field values in the array
// Ex: for 2 docs, count(fieldName) might return 5
// So we need to do count(EVAL(MV_MIN(fieldName))) instead
// to get accurate % of rows where field value exists
evalQuery: `${getSafeESQLName(`ne_${field.name}`)} = MV_MIN(${getSafeESQLName(
`${field.name}`
)})`,
query: `${getSafeESQLName(`${field.name}_count`)} = COUNT(${getSafeESQLName(
`ne_${field.name}`
)}),
${getSafeESQLName(`${field.name}_cardinality`)} = COUNT_DISTINCT(${getSafeESQLName(
field.name
)})`,
};
// +2 for count, and count_dictinct
startIndex += 2;
return result;
} else {
const result = {
...field,
startIndex,
query: `${getSafeESQLName(`${field.name}_count`)} = COUNT(${getSafeESQLName(
field.name
)})`,
};
// +1 for count for non-aggregatable field
startIndex += 1;
return result;
}
});
let countQuery = aggregatableFieldsToQuery.length > 0 ? '| STATS ' : '';
countQuery += aggregatableFieldsToQuery
.map((field) => {
// count idx = 0, cardinality idx = 1
return `${getSafeESQLName(`${field.name}_count`)} = COUNT(${getSafeESQLName(field.name)}),
${getSafeESQLName(`${field.name}_cardinality`)} = COUNT_DISTINCT(${getSafeESQLName(
field.name
)})`;
})
const evalQuery = fieldsToFetch
.map((field) => field.evalQuery)
.filter(isDefined)
.join(',');
let countQuery = fieldsToFetch.length > 0 ? '| STATS ' : '';
countQuery += fieldsToFetch.map((field) => field.query).join(',');
const query = esqlBaseQueryWithLimit + (evalQuery ? ' | EVAL ' + evalQuery : '') + countQuery;
const request = {
params: {
query: esqlBaseQueryWithLimit + countQuery,
query,
...(filter ? { filter } : {}),
},
};
try {
const esqlResults = await runRequest(request, { strategy: ESQL_SEARCH_STRATEGY });
const stats = {
@ -77,11 +124,14 @@ const getESQLOverallStatsInChunk = async ({
const sampleCount =
limitSize === 'none' || !isDefined(limitSize) ? totalCount : parseInt(limitSize, 10);
aggregatableFieldsToQuery.forEach((field, idx) => {
const count = esqlResultsResp.values[0][idx * 2] as number;
const cardinality = esqlResultsResp.values[0][idx * 2 + 1] as number;
fieldsToFetch.forEach((field, idx) => {
const count = esqlResultsResp.values[0][field.startIndex + aggToIndex.count] as number;
if (field.aggregatable === true) {
const cardinality = esqlResultsResp.values[0][
field.startIndex + aggToIndex.cardinality
] as number;
if (count > 0) {
stats.aggregatableExistsFields.push({
...field,
@ -102,14 +152,20 @@ const getESQLOverallStatsInChunk = async ({
});
}
} else {
const fieldData = {
fieldName: field.name,
existsInDocs: true,
};
if (count > 0) {
stats.nonAggregatableExistsFields.push(fieldData);
stats.nonAggregatableExistsFields.push({
fieldName: field.name,
existsInDocs: true,
stats: {
sampleCount,
count,
},
});
} else {
stats.nonAggregatableNotExistsFields.push(fieldData);
stats.nonAggregatableNotExistsFields.push({
fieldName: field.name,
existsInDocs: false,
});
}
}
});
@ -123,8 +179,8 @@ const getESQLOverallStatsInChunk = async ({
defaultMessage:
'Unable to fetch count & cardinality for {count} {count, plural, one {field} other {fields}}: {fieldNames}',
values: {
count: aggregatableFieldsToQuery.length,
fieldNames: aggregatableFieldsToQuery.map((r) => r.name).join(),
count: fieldsToFetch.length,
fieldNames: fieldsToFetch.map((r) => r.name).join(),
},
}),
});

View file

@ -24,7 +24,7 @@ interface Params {
* @param
* @returns
*/
export const getESQLTextFieldStats = async ({
export const getESQLExampleFieldValues = async ({
runRequest,
columns: textFields,
esqlBaseQuery,

View file

@ -8,15 +8,28 @@
import type { Filter } from '@kbn/es-query';
import type { Query } from '@kbn/data-plugin/common/query';
import type { SearchQueryLanguage } from '@kbn/ml-query-utils';
import type { FieldVisConfig } from '../../../../common/types/field_vis_config';
import type { RandomSamplerOption } from '../constants/random_sampler';
import type { DATA_VISUALIZER_INDEX_VIEWER } from '../constants/index_data_visualizer_viewer';
import type { OverallStats } from './overall_stats';
export interface DataVisualizerIndexBasedPageUrlState {
pageKey: typeof DATA_VISUALIZER_INDEX_VIEWER;
pageUrlState: Required<DataVisualizerIndexBasedAppState>;
}
export interface DataVisualizerPageState {
overallStats: OverallStats;
metricConfigs: FieldVisConfig[];
totalMetricFieldCount: number;
populatedMetricFieldCount: number;
metricsLoaded: boolean;
nonMetricConfigs: FieldVisConfig[];
nonMetricsLoaded: boolean;
documentCountStats?: FieldVisConfig;
}
export interface ListingPageUrlState {
pageSize: number;
pageIndex: number;

View file

@ -6,9 +6,9 @@
*/
import { type FrozenTierPreference } from '@kbn/ml-date-picker';
import type { ESQLDefaultLimitSizeOption } from '../components/search_panel/esql/limit_size';
import { type RandomSamplerOption } from '../constants/random_sampler';
import type { ESQLDefaultLimitSizeOption } from '../embeddables/grid_embeddable/types';
import { DATA_DRIFT_COMPARISON_CHART_TYPE } from './data_drift';
export const DV_FROZEN_TIER_PREFERENCE = 'dataVisualizer.frozenDataTierPreference';