mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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—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—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:
parent
3b08e74e58
commit
6010b64204
27 changed files with 1317 additions and 865 deletions
|
@ -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}
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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%
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>;
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
||||
/**
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -24,7 +24,7 @@ interface Params {
|
|||
* @param
|
||||
* @returns
|
||||
*/
|
||||
export const getESQLTextFieldStats = async ({
|
||||
export const getESQLExampleFieldValues = async ({
|
||||
runRequest,
|
||||
columns: textFields,
|
||||
esqlBaseQuery,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue