mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[ML] Add Field statistics embeddable as panel in Dashboard (#184030)
## Summary This PR adds Field statistics embeddable as panel in Dashboard By default, it will enable the ES|QL editor for the field stats panel. It will allow for editing of the ES|QL query, and time range.4b5438c7
-051f-4627-aab1-b802c23ca652e9bae0e4
-17cf-4a86-ad70-0da9d3667b53 If and only if ES|QL is disabled, it will show the data view picker as a fallback. ### 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> Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
This commit is contained in:
parent
d5a91fcc5d
commit
de027b80b2
36 changed files with 1534 additions and 95 deletions
|
@ -20,6 +20,7 @@ import {
|
|||
SmartFieldFallbackTooltip,
|
||||
} from '@kbn/unified-field-list';
|
||||
import type { DataVisualizerTableItem } from '@kbn/data-visualizer-plugin/public/application/common/components/stats_table/types';
|
||||
import { isOfAggregateQueryType } from '@kbn/es-query';
|
||||
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
|
||||
import { FIELD_STATISTICS_LOADED } from './constants';
|
||||
|
||||
|
@ -51,6 +52,7 @@ export const FieldStatisticsTable = React.memo((props: FieldStatisticsTableProps
|
|||
trackUiMetric,
|
||||
searchSessionId,
|
||||
additionalFieldGroups,
|
||||
timeRange,
|
||||
} = props;
|
||||
|
||||
const visibleFields = useMemo(
|
||||
|
@ -156,6 +158,7 @@ export const FieldStatisticsTable = React.memo((props: FieldStatisticsTableProps
|
|||
dataView={dataView}
|
||||
savedSearch={savedSearch}
|
||||
filters={filters}
|
||||
esqlQuery={isEsqlMode && isOfAggregateQueryType(query) ? query : undefined}
|
||||
query={query}
|
||||
visibleFieldNames={visibleFields}
|
||||
sessionId={searchSessionId}
|
||||
|
@ -166,8 +169,9 @@ export const FieldStatisticsTable = React.memo((props: FieldStatisticsTableProps
|
|||
showPreviewByDefault={showPreviewByDefault}
|
||||
onTableUpdate={updateState}
|
||||
renderFieldName={renderFieldName}
|
||||
esql={isEsqlMode}
|
||||
isEsqlMode={isEsqlMode}
|
||||
overridableServices={overridableServices}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
|
|
|
@ -8,8 +8,12 @@
|
|||
|
||||
import type { DataViewField, DataView } from '@kbn/data-views-plugin/public';
|
||||
import { type UiCounterMetricType } from '@kbn/analytics';
|
||||
import type { Filter, Query, AggregateQuery } from '@kbn/es-query';
|
||||
import type { SerializedTitles } from '@kbn/presentation-publishing';
|
||||
import type { Filter, Query, AggregateQuery, TimeRange } from '@kbn/es-query';
|
||||
import type {
|
||||
PublishesBlockingError,
|
||||
PublishesDataLoading,
|
||||
SerializedTitles,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import type { SavedSearch } from '@kbn/saved-search-plugin/public';
|
||||
import { type BehaviorSubject } from 'rxjs';
|
||||
import { DefaultEmbeddableApi } from '@kbn/embeddable-plugin/public';
|
||||
|
@ -116,7 +120,9 @@ interface FieldStatisticsTableEmbeddableComponentApi {
|
|||
|
||||
export type FieldStatisticsTableEmbeddableApi =
|
||||
DefaultEmbeddableApi<FieldStatisticsTableEmbeddableState> &
|
||||
FieldStatisticsTableEmbeddableComponentApi;
|
||||
FieldStatisticsTableEmbeddableComponentApi &
|
||||
PublishesDataLoading &
|
||||
PublishesBlockingError;
|
||||
|
||||
export interface FieldStatisticsTableProps {
|
||||
/**
|
||||
|
@ -173,4 +179,8 @@ export interface FieldStatisticsTableProps {
|
|||
* If table should query using ES|QL
|
||||
*/
|
||||
isEsqlMode?: boolean;
|
||||
/**
|
||||
* Time range
|
||||
*/
|
||||
timeRange?: TimeRange;
|
||||
}
|
||||
|
|
|
@ -664,6 +664,7 @@ export class SavedSearchEmbeddable
|
|||
isEsqlMode,
|
||||
});
|
||||
|
||||
const timeRange = this.getTimeRange();
|
||||
if (
|
||||
this.services.uiSettings.get(SHOW_FIELD_STATISTICS) === true &&
|
||||
viewMode === VIEW_MODE.AGGREGATED_LEVEL &&
|
||||
|
@ -683,6 +684,7 @@ export class SavedSearchEmbeddable
|
|||
onAddFilter={searchProps.onFilter}
|
||||
searchSessionId={this.input.searchSessionId}
|
||||
isEsqlMode={isEsqlMode}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
</KibanaRenderContextProvider>,
|
||||
|
|
|
@ -23,9 +23,10 @@ import { isFilterBasedDefaultQuery } from './filter_based_default_query';
|
|||
*/
|
||||
export function buildBaseFilterCriteria(
|
||||
timeFieldName?: string,
|
||||
earliestMs?: number,
|
||||
latestMs?: number,
|
||||
query?: Query['query']
|
||||
earliestMs?: number | string,
|
||||
latestMs?: number | string,
|
||||
query?: Query['query'],
|
||||
timeFormat = 'epoch_millis'
|
||||
): estypes.QueryDslQueryContainer[] {
|
||||
const filterCriteria = [];
|
||||
|
||||
|
@ -35,7 +36,7 @@ export function buildBaseFilterCriteria(
|
|||
[timeFieldName]: {
|
||||
gte: earliestMs,
|
||||
lte: latestMs,
|
||||
format: 'epoch_millis',
|
||||
format: timeFormat,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -211,8 +211,8 @@ export function isValidFieldStats(arg: unknown): arg is FieldStats {
|
|||
export interface FieldStatsCommonRequestParams {
|
||||
index: string;
|
||||
timeFieldName?: string;
|
||||
earliestMs?: number | undefined;
|
||||
latestMs?: number | undefined;
|
||||
earliestMs?: number | string | undefined;
|
||||
latestMs?: number | string | undefined;
|
||||
runtimeFieldMap?: estypes.MappingRuntimeFields;
|
||||
intervalMs?: number;
|
||||
query: estypes.QueryDslQueryContainer;
|
||||
|
@ -227,8 +227,8 @@ export type SupportedAggs = Set<string>;
|
|||
|
||||
export interface OverallStatsSearchStrategyParams {
|
||||
sessionId?: string;
|
||||
earliest?: number;
|
||||
latest?: number;
|
||||
earliest?: number | string;
|
||||
latest?: number | string;
|
||||
aggInterval: TimeBucketsInterval;
|
||||
intervalMs?: number;
|
||||
searchQuery: Query['query'];
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { Query } from '@kbn/es-query';
|
||||
import { buildBaseFilterCriteria } from '@kbn/ml-query-utils';
|
||||
|
||||
export const buildFilterCriteria = (
|
||||
timeFieldName?: string,
|
||||
earliestMs?: number | string,
|
||||
latestMs?: number | string,
|
||||
query?: Query['query']
|
||||
): estypes.QueryDslQueryContainer[] => {
|
||||
return buildBaseFilterCriteria(
|
||||
timeFieldName,
|
||||
earliestMs,
|
||||
latestMs,
|
||||
query,
|
||||
'epoch_millis||strict_date_optional_time'
|
||||
);
|
||||
};
|
|
@ -29,6 +29,7 @@
|
|||
"cloud"
|
||||
],
|
||||
"requiredBundles": [
|
||||
"dataViews",
|
||||
"kibanaReact",
|
||||
"kibanaUtils",
|
||||
"maps",
|
||||
|
@ -37,6 +38,7 @@
|
|||
"uiActions",
|
||||
"lens",
|
||||
"textBasedLanguages",
|
||||
"visualizations"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,8 +16,8 @@ import { createRandomSamplerWrapper } from '@kbn/ml-random-sampler-utils';
|
|||
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
|
||||
import { each, get } from 'lodash';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import { buildBaseFilterCriteria } from '@kbn/ml-query-utils';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { buildFilterCriteria } from '../../../../common/utils/build_query_filters';
|
||||
import { useDataVisualizerKibana } from '../../kibana_context';
|
||||
import { displayError } from '../util/display_error';
|
||||
|
||||
|
@ -70,7 +70,7 @@ export const getDocumentCountStatsRequest = (
|
|||
} = params;
|
||||
|
||||
const size = 0;
|
||||
const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, searchQuery);
|
||||
const filterCriteria = buildFilterCriteria(timeFieldName, earliestMs, latestMs, searchQuery);
|
||||
|
||||
const rawAggs: Record<string, estypes.AggregationsAggregationContainer> = {
|
||||
eventRate: {
|
||||
|
|
|
@ -20,6 +20,7 @@ import type { LensPublicStart } from '@kbn/lens-plugin/public';
|
|||
import type { IndexPatternFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public';
|
||||
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
import type { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/public';
|
||||
import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
|
||||
|
||||
export interface DataVisualizerSetupDependencies {
|
||||
home?: HomePublicPluginSetup;
|
||||
|
@ -42,4 +43,5 @@ export interface DataVisualizerStartDependencies {
|
|||
uiActions?: UiActionsStart;
|
||||
cloud?: CloudStart;
|
||||
savedSearch: SavedSearchPublicPluginStart;
|
||||
usageCollection?: UsageCollectionStart;
|
||||
}
|
||||
|
|
|
@ -73,7 +73,7 @@ import {
|
|||
RANDOM_SAMPLER_OPTION,
|
||||
type RandomSamplerOption,
|
||||
} from '../../constants/random_sampler';
|
||||
import type { FieldStatisticsTableEmbeddableState } from '../../embeddables/grid_embeddable/types';
|
||||
import type { FieldStatisticTableEmbeddableProps } from '../../embeddables/grid_embeddable/types';
|
||||
|
||||
const defaultSearchQuery = {
|
||||
match_all: {},
|
||||
|
@ -217,7 +217,7 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
|
|||
});
|
||||
};
|
||||
|
||||
const input: Required<FieldStatisticsTableEmbeddableState, 'dataView'> = useMemo(() => {
|
||||
const input: Required<FieldStatisticTableEmbeddableProps, 'dataView'> = useMemo(() => {
|
||||
return {
|
||||
dataView: currentDataView,
|
||||
savedSearch: currentSavedSearch,
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const FIELD_STATS_EMBEDDABLE_TYPE = 'field_stats_table';
|
||||
export const FIELD_STATS_DATA_VIEW_REF_NAME = 'fieldStatsTableDataViewId';
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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, { useRef, useState, useCallback } from 'react';
|
||||
import { TextBasedLangEditor } from '@kbn/text-based-languages/public';
|
||||
import { EuiFlexItem } from '@elastic/eui';
|
||||
import type { AggregateQuery } from '@kbn/es-query';
|
||||
|
||||
const expandCodeEditor = (status: boolean) => {};
|
||||
|
||||
interface FieldStatsESQLEditorProps {
|
||||
canEditTextBasedQuery?: boolean;
|
||||
query: AggregateQuery;
|
||||
setQuery: (query: AggregateQuery) => void;
|
||||
onQuerySubmit: (query: AggregateQuery, abortController: AbortController) => Promise<void>;
|
||||
}
|
||||
export const FieldStatsESQLEditor = ({
|
||||
canEditTextBasedQuery = true,
|
||||
query,
|
||||
setQuery,
|
||||
onQuerySubmit,
|
||||
}: FieldStatsESQLEditorProps) => {
|
||||
const prevQuery = useRef<AggregateQuery>(query);
|
||||
const [isVisualizationLoading, setIsVisualizationLoading] = useState(false);
|
||||
|
||||
const onTextLangQuerySubmit = useCallback(
|
||||
async (q, abortController) => {
|
||||
if (q && onQuerySubmit) {
|
||||
setIsVisualizationLoading(true);
|
||||
await onQuerySubmit(q, abortController);
|
||||
setIsVisualizationLoading(false);
|
||||
}
|
||||
},
|
||||
[onQuerySubmit]
|
||||
);
|
||||
|
||||
if (!canEditTextBasedQuery) return null;
|
||||
|
||||
return (
|
||||
<EuiFlexItem grow={false} data-test-subj="InlineEditingESQLEditor">
|
||||
<TextBasedLangEditor
|
||||
query={query}
|
||||
onTextLangQueryChange={(q) => {
|
||||
setQuery(q);
|
||||
prevQuery.current = q;
|
||||
}}
|
||||
expandCodeEditor={expandCodeEditor}
|
||||
isCodeEditorExpanded
|
||||
hideMinimizeButton
|
||||
editorIsInline
|
||||
hideRunQueryText
|
||||
onTextLangQuerySubmit={onTextLangQuerySubmit}
|
||||
isDisabled={false}
|
||||
allowQueryCancellation
|
||||
isLoading={isVisualizationLoading}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,392 @@
|
|||
/*
|
||||
* 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 type { Reference } from '@kbn/content-management-utils';
|
||||
import type { StartServicesAccessor } from '@kbn/core-lifecycle-browser';
|
||||
import {
|
||||
APPLY_FILTER_TRIGGER,
|
||||
generateFilters,
|
||||
type DataPublicPluginStart,
|
||||
} from '@kbn/data-plugin/public';
|
||||
import type { DataViewField } from '@kbn/data-views-plugin/common';
|
||||
import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/common';
|
||||
import type { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
apiHasExecutionContext,
|
||||
fetch$,
|
||||
initializeTimeRange,
|
||||
initializeTitles,
|
||||
useBatchedPublishingSubjects,
|
||||
useFetchContext,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import React, { useEffect } from 'react';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
map,
|
||||
skipWhile,
|
||||
Subscription,
|
||||
skip,
|
||||
switchMap,
|
||||
distinctUntilChanged,
|
||||
} from 'rxjs';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { dynamic } from '@kbn/shared-ux-utility';
|
||||
import { isDefined } from '@kbn/ml-is-defined';
|
||||
import { EuiFlexItem } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import type { ActionExecutionContext } from '@kbn/ui-actions-plugin/public';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import { FilterStateStore } from '@kbn/es-query';
|
||||
import { getESQLAdHocDataview, getIndexPatternFromESQLQuery } from '@kbn/esql-utils';
|
||||
import { ACTION_GLOBAL_APPLY_FILTER } from '@kbn/unified-search-plugin/public';
|
||||
import type { DataVisualizerTableState } from '../../../../../common/types';
|
||||
import type { DataVisualizerPluginStart } from '../../../../plugin';
|
||||
import type { FieldStatisticsTableEmbeddableState } from '../grid_embeddable/types';
|
||||
import { FieldStatsInitializerViewType } from '../grid_embeddable/types';
|
||||
import { FIELD_STATS_EMBEDDABLE_TYPE, FIELD_STATS_DATA_VIEW_REF_NAME } from './constants';
|
||||
import { initializeFieldStatsControls } from './initialize_field_stats_controls';
|
||||
import type { DataVisualizerStartDependencies } from '../../../common/types/data_visualizer_plugin';
|
||||
import type { FieldStatisticsTableEmbeddableApi } from './types';
|
||||
import { isESQLQuery } from '../../search_strategy/requests/esql_utils';
|
||||
|
||||
export interface EmbeddableFieldStatsChartStartServices {
|
||||
data: DataPublicPluginStart;
|
||||
}
|
||||
export type EmbeddableFieldStatsChartType = typeof FIELD_STATS_EMBEDDABLE_TYPE;
|
||||
|
||||
const FieldStatisticsWrapper = dynamic(() => import('../grid_embeddable/field_stats_wrapper'));
|
||||
|
||||
const ERROR_MSG = {
|
||||
APPLY_FILTER_ERR: i18n.translate('xpack.dataVisualizer.fieldStats.errors.errorApplyingFilter', {
|
||||
defaultMessage: 'Error applying filter',
|
||||
}),
|
||||
UPDATE_CONFIG_ERROR: i18n.translate(
|
||||
'xpack.dataVisualizer.fieldStats.errors.errorUpdatingConfig',
|
||||
{
|
||||
defaultMessage: 'Error updating settings for field statistics.',
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
export const getDependencies = async (
|
||||
getStartServices: StartServicesAccessor<
|
||||
DataVisualizerStartDependencies,
|
||||
DataVisualizerPluginStart
|
||||
>
|
||||
) => {
|
||||
const [
|
||||
{ http, uiSettings, notifications, ...startServices },
|
||||
{ lens, data, usageCollection, fieldFormats },
|
||||
] = await getStartServices();
|
||||
|
||||
return {
|
||||
http,
|
||||
uiSettings,
|
||||
data,
|
||||
notifications,
|
||||
lens,
|
||||
usageCollection,
|
||||
fieldFormats,
|
||||
...startServices,
|
||||
};
|
||||
};
|
||||
|
||||
export const getFieldStatsChartEmbeddableFactory = (
|
||||
getStartServices: StartServicesAccessor<
|
||||
DataVisualizerStartDependencies,
|
||||
DataVisualizerPluginStart
|
||||
>
|
||||
) => {
|
||||
const factory: ReactEmbeddableFactory<
|
||||
FieldStatisticsTableEmbeddableState,
|
||||
FieldStatisticsTableEmbeddableState,
|
||||
FieldStatisticsTableEmbeddableApi
|
||||
> = {
|
||||
type: FIELD_STATS_EMBEDDABLE_TYPE,
|
||||
deserializeState: (state) => {
|
||||
const serializedState = cloneDeep(state.rawState);
|
||||
// inject the reference
|
||||
const dataViewIdRef = state.references?.find(
|
||||
(ref) => ref.name === FIELD_STATS_DATA_VIEW_REF_NAME
|
||||
);
|
||||
// if the serializedState already contains a dataViewId, we don't want to overwrite it. (Unsaved state can cause this)
|
||||
if (dataViewIdRef && serializedState && !serializedState.dataViewId) {
|
||||
serializedState.dataViewId = dataViewIdRef?.id;
|
||||
}
|
||||
return serializedState;
|
||||
},
|
||||
buildEmbeddable: async (state, buildApi, uuid, parentApi) => {
|
||||
const [coreStart, pluginStart] = await getStartServices();
|
||||
|
||||
const { http, uiSettings, notifications, ...startServices } = coreStart;
|
||||
const { lens, data, usageCollection, fieldFormats } = pluginStart;
|
||||
|
||||
const deps = {
|
||||
http,
|
||||
uiSettings,
|
||||
data,
|
||||
notifications,
|
||||
lens,
|
||||
usageCollection,
|
||||
fieldFormats,
|
||||
...startServices,
|
||||
};
|
||||
const {
|
||||
api: timeRangeApi,
|
||||
comparators: timeRangeComparators,
|
||||
serialize: serializeTimeRange,
|
||||
} = initializeTimeRange(state);
|
||||
|
||||
const { titlesApi, titleComparators, serializeTitles } = initializeTitles(state);
|
||||
|
||||
const {
|
||||
fieldStatsControlsApi,
|
||||
dataLoadingApi,
|
||||
fieldStatsControlsComparators,
|
||||
serializeFieldStatsChartState,
|
||||
onFieldStatsTableDestroy,
|
||||
resetData$,
|
||||
} = initializeFieldStatsControls(state);
|
||||
const { onError, dataLoading, blockingError } = dataLoadingApi;
|
||||
|
||||
const defaultDataViewId = await deps.data.dataViews.getDefaultId();
|
||||
const validDataViewId: string =
|
||||
isDefined(state.dataViewId) && state.dataViewId !== ''
|
||||
? state.dataViewId
|
||||
: defaultDataViewId ?? '';
|
||||
let initialDataView: DataView[] | undefined;
|
||||
try {
|
||||
const dataView = isESQLQuery(state.query)
|
||||
? await getESQLAdHocDataview(
|
||||
getIndexPatternFromESQLQuery(state.query.esql),
|
||||
deps.data.dataViews
|
||||
)
|
||||
: await deps.data.dataViews.get(validDataViewId);
|
||||
initialDataView = [dataView];
|
||||
} catch (error) {
|
||||
// Only need to publish blocking error if viewtype is data view, and no data view found
|
||||
if (state.viewType === FieldStatsInitializerViewType.DATA_VIEW) {
|
||||
onError(error);
|
||||
}
|
||||
}
|
||||
|
||||
const dataViews$ = new BehaviorSubject<DataView[] | undefined>(initialDataView);
|
||||
|
||||
const subscriptions = new Subscription();
|
||||
if (fieldStatsControlsApi.dataViewId$) {
|
||||
subscriptions.add(
|
||||
fieldStatsControlsApi.dataViewId$
|
||||
.pipe(
|
||||
skip(1),
|
||||
skipWhile((dataViewId) => !dataViewId && !defaultDataViewId),
|
||||
switchMap(async (dataViewId) => {
|
||||
try {
|
||||
return await deps.data.dataViews.get(dataViewId ?? defaultDataViewId);
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
})
|
||||
)
|
||||
.subscribe((nextSelectedDataView) => {
|
||||
if (nextSelectedDataView) {
|
||||
dataViews$.next([nextSelectedDataView]);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const { toasts } = deps.notifications;
|
||||
|
||||
const api = buildApi(
|
||||
{
|
||||
...timeRangeApi,
|
||||
...titlesApi,
|
||||
...fieldStatsControlsApi,
|
||||
// PublishesDataLoading
|
||||
dataLoading,
|
||||
// PublishesBlockingError
|
||||
blockingError,
|
||||
getTypeDisplayName: () =>
|
||||
i18n.translate('xpack.dataVisualizer.fieldStats.typeDisplayName', {
|
||||
defaultMessage: 'field statistics',
|
||||
}),
|
||||
isEditingEnabled: () => true,
|
||||
onEdit: async () => {
|
||||
try {
|
||||
const { resolveEmbeddableFieldStatsUserInput } = await import(
|
||||
'./resolve_field_stats_embeddable_input'
|
||||
);
|
||||
const chartState = serializeFieldStatsChartState();
|
||||
const nextUpdate = await resolveEmbeddableFieldStatsUserInput(
|
||||
coreStart,
|
||||
pluginStart,
|
||||
parentApi,
|
||||
uuid,
|
||||
false,
|
||||
chartState,
|
||||
fieldStatsControlsApi
|
||||
);
|
||||
fieldStatsControlsApi.updateUserInput(nextUpdate);
|
||||
} catch (e) {
|
||||
toasts.addError(e, { title: ERROR_MSG.UPDATE_CONFIG_ERROR });
|
||||
}
|
||||
},
|
||||
dataViews: dataViews$,
|
||||
serializeState: () => {
|
||||
const dataViewId = fieldStatsControlsApi.dataViewId$?.getValue();
|
||||
const references: Reference[] = dataViewId
|
||||
? [
|
||||
{
|
||||
type: DATA_VIEW_SAVED_OBJECT_TYPE,
|
||||
name: FIELD_STATS_DATA_VIEW_REF_NAME,
|
||||
id: dataViewId,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
return {
|
||||
rawState: {
|
||||
...serializeTitles(),
|
||||
...serializeTimeRange(),
|
||||
...serializeFieldStatsChartState(),
|
||||
},
|
||||
references,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
...timeRangeComparators,
|
||||
...titleComparators,
|
||||
...fieldStatsControlsComparators,
|
||||
}
|
||||
);
|
||||
|
||||
const reload$ = fetch$(api).pipe(
|
||||
skipWhile((fetchContext) => !fetchContext.isReload),
|
||||
map(() => Date.now())
|
||||
);
|
||||
const reset$ = resetData$.pipe(skip(1), distinctUntilChanged());
|
||||
|
||||
const onTableUpdate = (changes: Partial<DataVisualizerTableState>) => {
|
||||
if (isDefined(changes?.showDistributions)) {
|
||||
fieldStatsControlsApi.showDistributions$.next(changes.showDistributions);
|
||||
}
|
||||
};
|
||||
|
||||
const addFilters = (filters: Filter[], actionId: string = ACTION_GLOBAL_APPLY_FILTER) => {
|
||||
if (!pluginStart.uiActions) {
|
||||
toasts.addWarning(ERROR_MSG.APPLY_FILTER_ERR);
|
||||
return;
|
||||
}
|
||||
const trigger = pluginStart.uiActions.getTrigger(APPLY_FILTER_TRIGGER);
|
||||
if (!trigger) {
|
||||
toasts.addWarning(ERROR_MSG.APPLY_FILTER_ERR);
|
||||
return;
|
||||
}
|
||||
const actionContext = {
|
||||
embeddable: api,
|
||||
trigger,
|
||||
} as ActionExecutionContext;
|
||||
|
||||
const executeContext = {
|
||||
...actionContext,
|
||||
filters,
|
||||
};
|
||||
try {
|
||||
const action = pluginStart.uiActions.getAction(actionId);
|
||||
action.execute(executeContext);
|
||||
} catch (error) {
|
||||
toasts.addWarning(ERROR_MSG.APPLY_FILTER_ERR);
|
||||
}
|
||||
};
|
||||
|
||||
const statsTableCss = css({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
overflowY: 'auto',
|
||||
});
|
||||
|
||||
return {
|
||||
api,
|
||||
Component: () => {
|
||||
if (!apiHasExecutionContext(parentApi)) {
|
||||
onError(new Error('Parent API does not have execution context'));
|
||||
}
|
||||
|
||||
const { filters: globalFilters, query: globalQuery, timeRange } = useFetchContext(api);
|
||||
const [dataViews, esqlQuery, viewType, showPreviewByDefault] =
|
||||
useBatchedPublishingSubjects(
|
||||
api.dataViews,
|
||||
api.query$,
|
||||
api.viewType$,
|
||||
api.showDistributions$
|
||||
);
|
||||
const lastReloadRequestTime = useObservable(reload$, Date.now());
|
||||
|
||||
const isEsqlMode = viewType === FieldStatsInitializerViewType.ESQL;
|
||||
|
||||
const dataView =
|
||||
Array.isArray(dataViews) && dataViews.length > 0 ? dataViews[0] : undefined;
|
||||
const onAddFilter = (
|
||||
field: string | DataViewField,
|
||||
value: string,
|
||||
operator: '+' | '-'
|
||||
) => {
|
||||
if (!dataView || !pluginStart.data) {
|
||||
toasts.addWarning(ERROR_MSG.APPLY_FILTER_ERR);
|
||||
return;
|
||||
}
|
||||
|
||||
let filters = generateFilters(
|
||||
pluginStart.data.query.filterManager,
|
||||
field,
|
||||
value,
|
||||
operator,
|
||||
dataView
|
||||
);
|
||||
filters = filters.map((filter) => ({
|
||||
...filter,
|
||||
$state: { store: FilterStateStore.APP_STATE },
|
||||
}));
|
||||
addFilters(filters);
|
||||
};
|
||||
|
||||
// On destroy
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
subscriptions?.unsubscribe();
|
||||
onFieldStatsTableDestroy();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EuiFlexItem css={statsTableCss} data-test-subj="dashboardFieldStatsEmbeddedContent">
|
||||
<FieldStatisticsWrapper
|
||||
shouldGetSubfields={false}
|
||||
dataView={dataView}
|
||||
esqlQuery={esqlQuery}
|
||||
query={globalQuery}
|
||||
filters={globalFilters}
|
||||
lastReloadRequestTime={lastReloadRequestTime}
|
||||
isEsqlMode={isEsqlMode}
|
||||
onTableUpdate={onTableUpdate}
|
||||
showPreviewByDefault={showPreviewByDefault}
|
||||
onAddFilter={onAddFilter}
|
||||
resetData$={reset$}
|
||||
timeRange={timeRange}
|
||||
onRenderComplete={dataLoadingApi.onRenderComplete}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
return factory;
|
||||
};
|
|
@ -0,0 +1,301 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiCodeBlock,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutFooter,
|
||||
EuiFlyoutHeader,
|
||||
EuiFormRow,
|
||||
EuiTitle,
|
||||
EuiSpacer,
|
||||
EuiIconTip,
|
||||
EuiCallOut,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { FC } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import React, { useMemo, useState, useCallback } from 'react';
|
||||
import { ENABLE_ESQL, getESQLAdHocDataview, getIndexPatternFromESQLQuery } from '@kbn/esql-utils';
|
||||
import type { AggregateQuery } from '@kbn/es-query';
|
||||
import { css } from '@emotion/react';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { useDataVisualizerKibana } from '../../../kibana_context';
|
||||
import { FieldStatsESQLEditor } from './field_stats_esql_editor';
|
||||
import type {
|
||||
FieldStatisticsTableEmbeddableState,
|
||||
FieldStatsInitialState,
|
||||
} from '../grid_embeddable/types';
|
||||
import { FieldStatsInitializerViewType } from '../grid_embeddable/types';
|
||||
import { isESQLQuery } from '../../search_strategy/requests/esql_utils';
|
||||
import { DataSourceTypeSelector } from './field_stats_initializer_view_type';
|
||||
|
||||
export interface FieldStatsInitializerProps {
|
||||
initialInput?: Partial<FieldStatisticsTableEmbeddableState>;
|
||||
onCreate: (props: FieldStatsInitialState) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
onPreview: (update: Partial<FieldStatsInitialState>) => Promise<void>;
|
||||
isNewPanel: boolean;
|
||||
}
|
||||
|
||||
const defaultESQLQuery = { esql: '' };
|
||||
const defaultTitle = i18n.translate('xpack.dataVisualizer.fieldStatistics.displayName', {
|
||||
defaultMessage: 'Field statistics',
|
||||
});
|
||||
|
||||
const isScrollable = false;
|
||||
export const FieldStatisticsInitializer: FC<FieldStatsInitializerProps> = ({
|
||||
initialInput,
|
||||
onCreate,
|
||||
onCancel,
|
||||
onPreview,
|
||||
isNewPanel,
|
||||
}) => {
|
||||
const {
|
||||
data: { dataViews },
|
||||
unifiedSearch: {
|
||||
ui: { IndexPatternSelect },
|
||||
},
|
||||
uiSettings,
|
||||
} = useDataVisualizerKibana().services;
|
||||
|
||||
const [dataViewId, setDataViewId] = useState(initialInput?.dataViewId ?? '');
|
||||
const [viewType, setViewType] = useState(
|
||||
initialInput?.viewType ?? FieldStatsInitializerViewType.DATA_VIEW
|
||||
);
|
||||
const [esqlQuery, setQuery] = useState<AggregateQuery>(initialInput?.query ?? defaultESQLQuery);
|
||||
const isEsqlEnabled = useMemo(() => uiSettings.get(ENABLE_ESQL), [uiSettings]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialInput?.viewType === undefined) {
|
||||
// By default, if ES|QL is enabled, then use ES|QL
|
||||
setViewType(
|
||||
isEsqlEnabled ? FieldStatsInitializerViewType.ESQL : FieldStatsInitializerViewType.DATA_VIEW
|
||||
);
|
||||
}
|
||||
}, [isEsqlEnabled, initialInput?.viewType]);
|
||||
|
||||
const isEsqlMode = viewType === FieldStatsInitializerViewType.ESQL;
|
||||
const updatedProps = useMemo(() => {
|
||||
return {
|
||||
viewType,
|
||||
title: initialInput?.title ?? defaultTitle,
|
||||
dataViewId,
|
||||
query: isEsqlMode ? esqlQuery : undefined,
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dataViewId, viewType, esqlQuery.esql, isEsqlMode]);
|
||||
const onESQLQuerySubmit = useCallback(
|
||||
async (query: AggregateQuery, abortController: AbortController) => {
|
||||
const adhocDataView = await getESQLAdHocDataview(
|
||||
getIndexPatternFromESQLQuery(query.esql),
|
||||
dataViews
|
||||
);
|
||||
if (adhocDataView && adhocDataView.id) {
|
||||
setDataViewId(adhocDataView.id);
|
||||
}
|
||||
|
||||
await onPreview({
|
||||
viewType,
|
||||
dataViewId: adhocDataView?.id,
|
||||
query,
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[isEsqlMode]
|
||||
);
|
||||
const isEsqlFormValid = isEsqlMode ? isEsqlEnabled && isESQLQuery(esqlQuery) : true;
|
||||
const isDataViewFormValid =
|
||||
viewType === FieldStatsInitializerViewType.DATA_VIEW ? dataViewId !== '' : true;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlyoutHeader
|
||||
hasBorder={true}
|
||||
css={css`
|
||||
pointer-events: auto;
|
||||
background-color: ${euiThemeVars.euiColorEmptyShade};
|
||||
`}
|
||||
data-test-subj="editFlyoutHeader"
|
||||
>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xs" data-test-subj="inlineEditingFlyoutLabel">
|
||||
<h2>
|
||||
{isNewPanel
|
||||
? i18n.translate(
|
||||
'xpack.dataVisualizer.fieldStatisticsDashboardPanel.config.createLable',
|
||||
{
|
||||
defaultMessage: 'Create field statistics',
|
||||
}
|
||||
)
|
||||
: i18n.translate(
|
||||
'xpack.dataVisualizer.fieldStatisticsDashboardPanel.config.editLabel',
|
||||
{
|
||||
defaultMessage: 'Edit field statistics',
|
||||
}
|
||||
)}{' '}
|
||||
<EuiIconTip
|
||||
type="iInCircle"
|
||||
content={i18n.translate(
|
||||
'xpack.dataVisualizer.fieldStatisticsDashboardPanel.config.samplingTooltip',
|
||||
{
|
||||
defaultMessage:
|
||||
'Field statistics uses the random sampling aggregation to increase performance, but some accuracy might be lost.',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody
|
||||
className="lnsEditFlyoutBody"
|
||||
css={css`
|
||||
// styles needed to display extra drop targets that are outside of the config panel main area
|
||||
overflow-y: auto;
|
||||
padding-left: ${euiThemeVars.euiFormMaxWidth};
|
||||
margin-left: -${euiThemeVars.euiFormMaxWidth};
|
||||
pointer-events: none;
|
||||
.euiFlyoutBody__overflow {
|
||||
-webkit-mask-image: none;
|
||||
padding-left: inherit;
|
||||
margin-left: inherit;
|
||||
${!isScrollable &&
|
||||
`
|
||||
overflow-y: hidden;
|
||||
`}
|
||||
> * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
.euiFlyoutBody__overflowContent {
|
||||
padding: 0;
|
||||
block-size: 100%;
|
||||
}
|
||||
border-bottom: 2px solid ${euiThemeVars.euiBorderColor};
|
||||
`}
|
||||
>
|
||||
<EuiFlexGroup
|
||||
css={css`
|
||||
block-size: 100%;
|
||||
`}
|
||||
direction="column"
|
||||
gutterSize="none"
|
||||
>
|
||||
{isNewPanel ? (
|
||||
<EuiCallOut
|
||||
size="s"
|
||||
iconType="iInCircle"
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.dataVisualizer.fieldStatisticsDashboardPanel.config.description"
|
||||
defaultMessage="The visualization displays summarized information and statistics to show how each field in your data is populated."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
{initialInput?.viewType === FieldStatsInitializerViewType.ESQL && !isEsqlEnabled ? (
|
||||
<>
|
||||
<DataSourceTypeSelector value={viewType} onChange={setViewType} />
|
||||
</>
|
||||
) : null}
|
||||
{viewType === FieldStatsInitializerViewType.ESQL && !isEsqlEnabled ? (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiCodeBlock>{esqlQuery.esql}</EuiCodeBlock>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{viewType === FieldStatsInitializerViewType.DATA_VIEW ? (
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={i18n.translate(
|
||||
'xpack.dataVisualizer.fieldStatisticsDashboardPanel.dataViewLabel',
|
||||
{
|
||||
defaultMessage: 'Data view',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<IndexPatternSelect
|
||||
autoFocus={!dataViewId}
|
||||
fullWidth
|
||||
compressed
|
||||
indexPatternId={dataViewId}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.dataVisualizer.fieldStatisticsDashboardPanel.dataViewSelectorPlaceholder',
|
||||
{
|
||||
defaultMessage: 'Select data view',
|
||||
}
|
||||
)}
|
||||
onChange={(newId) => {
|
||||
setDataViewId(newId ?? '');
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
) : null}
|
||||
{isEsqlMode && isEsqlEnabled ? (
|
||||
<FieldStatsESQLEditor
|
||||
query={esqlQuery}
|
||||
setQuery={setQuery}
|
||||
onQuerySubmit={onESQLQuerySubmit}
|
||||
/>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutBody>
|
||||
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
onClick={onCancel}
|
||||
data-test-subj="fieldStatsInitializerCancelButton"
|
||||
flush="left"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.dataVisualizer.fieldStatisticsDashboardPanel.config.cancelButtonAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Cancel applied changes',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.dataVisualizer.fieldStatisticsDashboardPanel.config.cancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
onClick={onCreate.bind(null, updatedProps)}
|
||||
fill
|
||||
aria-label={i18n.translate(
|
||||
'xpack.dataVisualizer.fieldStatisticsDashboardPanel.config.applyFlyoutAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Apply changes',
|
||||
}
|
||||
)}
|
||||
disabled={!isEsqlFormValid || !isDataViewFormValid}
|
||||
iconType="check"
|
||||
data-test-subj="applyFlyoutButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.dataVisualizer.fieldStatisticsDashboardPanel.config.applyAndCloseLabel"
|
||||
defaultMessage="Apply and close"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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 type { FC } from 'react';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiButtonGroup, EuiFormRow, type EuiButtonGroupOptionProps } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FieldStatsInitializerViewType } from '../grid_embeddable/types';
|
||||
|
||||
const viewTypeOptions: EuiButtonGroupOptionProps[] = [
|
||||
{
|
||||
id: FieldStatsInitializerViewType.DATA_VIEW,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id="xpack.dataVisualizer.fieldStatsDashboardPanel.dataSourceSelector.dataViewLabel"
|
||||
defaultMessage="Data view"
|
||||
/>
|
||||
),
|
||||
iconType: 'visLine',
|
||||
},
|
||||
{
|
||||
id: FieldStatsInitializerViewType.ESQL,
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id="xpack.dataVisualizer.fieldStatsDashboardPanel.dataSourceSelector.esqlLabel"
|
||||
defaultMessage="ES|QL"
|
||||
/>
|
||||
),
|
||||
iconType: 'visTable',
|
||||
},
|
||||
];
|
||||
|
||||
const dataSourceLabel = i18n.translate(
|
||||
'xpack.dataVisualizer.fieldStatsDashboardPanel.dataSourceLabel',
|
||||
{
|
||||
defaultMessage: 'Data source',
|
||||
}
|
||||
);
|
||||
|
||||
const dataSourceAriaLabel = i18n.translate(
|
||||
'xpack.dataVisualizer.fieldStatsDashboardPanel.viewTypeLabel',
|
||||
{
|
||||
defaultMessage: 'Pick type of data source to use',
|
||||
}
|
||||
);
|
||||
|
||||
export interface ViewTypeSelectorProps {
|
||||
value: FieldStatsInitializerViewType;
|
||||
onChange: (update: FieldStatsInitializerViewType) => void;
|
||||
}
|
||||
|
||||
export const DataSourceTypeSelector: FC<ViewTypeSelectorProps> = ({ value, onChange }) => {
|
||||
return (
|
||||
<EuiFormRow fullWidth label={dataSourceLabel}>
|
||||
<EuiButtonGroup
|
||||
isFullWidth
|
||||
aria-label={dataSourceAriaLabel}
|
||||
options={viewTypeOptions}
|
||||
idSelected={value}
|
||||
onChange={onChange as (id: string) => void}
|
||||
legend={dataSourceAriaLabel}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 type { EmbeddableSetup } from '@kbn/embeddable-plugin/public';
|
||||
import type { DataVisualizerCoreSetup } from '../../../../plugin';
|
||||
import { FIELD_STATS_EMBEDDABLE_TYPE } from './constants';
|
||||
|
||||
export const registerEmbeddables = (embeddable: EmbeddableSetup, core: DataVisualizerCoreSetup) => {
|
||||
embeddable.registerReactEmbeddableFactory(FIELD_STATS_EMBEDDABLE_TYPE, async () => {
|
||||
const { getFieldStatsChartEmbeddableFactory } = await import('./field_stats_factory');
|
||||
return getFieldStatsChartEmbeddableFactory(core.getStartServices);
|
||||
});
|
||||
};
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
/*
|
||||
* 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 type { AggregateQuery } from '@kbn/es-query';
|
||||
import type { StateComparators } from '@kbn/presentation-publishing';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import fastIsEqual from 'fast-deep-equal';
|
||||
import { FieldStatsInitializerViewType } from '../grid_embeddable/types';
|
||||
import type { FieldStatsInitialState } from '../grid_embeddable/types';
|
||||
import type { FieldStatsControlsApi } from './types';
|
||||
|
||||
export const initializeFieldStatsControls = (rawState: FieldStatsInitialState) => {
|
||||
const viewType$ = new BehaviorSubject<FieldStatsInitializerViewType | undefined>(
|
||||
rawState.viewType ?? FieldStatsInitializerViewType.ESQL
|
||||
);
|
||||
const dataViewId$ = new BehaviorSubject<string | undefined>(rawState.dataViewId);
|
||||
const query$ = new BehaviorSubject<AggregateQuery | undefined>(rawState.query);
|
||||
const showDistributions$ = new BehaviorSubject<boolean | undefined>(rawState.showDistributions);
|
||||
const resetData$ = new BehaviorSubject<number>(Date.now());
|
||||
|
||||
const dataLoading$ = new BehaviorSubject<boolean | undefined>(true);
|
||||
const blockingError = new BehaviorSubject<Error | undefined>(undefined);
|
||||
|
||||
const updateUserInput = (update: FieldStatsInitialState, shouldResetData = false) => {
|
||||
if (shouldResetData) {
|
||||
resetData$.next(Date.now());
|
||||
}
|
||||
viewType$.next(update.viewType);
|
||||
dataViewId$.next(update.dataViewId);
|
||||
query$.next(update.query);
|
||||
};
|
||||
|
||||
const serializeFieldStatsChartState = (): FieldStatsInitialState => {
|
||||
return {
|
||||
viewType: viewType$.getValue(),
|
||||
dataViewId: dataViewId$.getValue(),
|
||||
query: query$.getValue(),
|
||||
showDistributions: showDistributions$.getValue(),
|
||||
};
|
||||
};
|
||||
|
||||
const fieldStatsControlsComparators: StateComparators<FieldStatsInitialState> = {
|
||||
viewType: [viewType$, (arg) => viewType$.next(arg)],
|
||||
dataViewId: [dataViewId$, (arg) => dataViewId$.next(arg)],
|
||||
query: [query$, (arg) => query$.next(arg), fastIsEqual],
|
||||
showDistributions: [showDistributions$, (arg) => showDistributions$.next(arg)],
|
||||
};
|
||||
|
||||
const onRenderComplete = () => dataLoading$.next(false);
|
||||
const onLoading = (v: boolean) => dataLoading$.next(v);
|
||||
const onError = (error?: Error) => blockingError.next(error);
|
||||
|
||||
return {
|
||||
fieldStatsControlsApi: {
|
||||
viewType$,
|
||||
dataViewId$,
|
||||
query$,
|
||||
updateUserInput,
|
||||
showDistributions$,
|
||||
} as unknown as FieldStatsControlsApi,
|
||||
dataLoadingApi: {
|
||||
dataLoading: dataLoading$,
|
||||
onRenderComplete,
|
||||
onLoading,
|
||||
onError,
|
||||
blockingError,
|
||||
},
|
||||
// Reset data is internal state management, so no need to expose this in api
|
||||
resetData$,
|
||||
serializeFieldStatsChartState,
|
||||
fieldStatsControlsComparators,
|
||||
onFieldStatsTableDestroy: () => {
|
||||
viewType$.complete();
|
||||
dataViewId$.complete();
|
||||
query$.complete();
|
||||
resetData$.complete();
|
||||
},
|
||||
};
|
||||
};
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* 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 type { CoreStart } from '@kbn/core/public';
|
||||
import { tracksOverlays } from '@kbn/presentation-containers';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
import React from 'react';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils';
|
||||
import { isDefined } from '@kbn/ml-is-defined';
|
||||
import { FieldStatisticsInitializer } from './field_stats_initializer';
|
||||
import type { DataVisualizerStartDependencies } from '../../../common/types/data_visualizer_plugin';
|
||||
import type {
|
||||
FieldStatisticsTableEmbeddableState,
|
||||
FieldStatsInitialState,
|
||||
} from '../grid_embeddable/types';
|
||||
import type { FieldStatsControlsApi } from './types';
|
||||
import { getOrCreateDataViewByIndexPattern } from '../../search_strategy/requests/get_data_view_by_index_pattern';
|
||||
|
||||
export async function resolveEmbeddableFieldStatsUserInput(
|
||||
coreStart: CoreStart,
|
||||
pluginStart: DataVisualizerStartDependencies,
|
||||
parentApi: unknown,
|
||||
focusedPanelId: string,
|
||||
isNewPanel: boolean,
|
||||
initialState?: FieldStatisticsTableEmbeddableState,
|
||||
fieldStatsControlsApi?: FieldStatsControlsApi
|
||||
): Promise<FieldStatisticsTableEmbeddableState> {
|
||||
const { overlays } = coreStart;
|
||||
|
||||
const overlayTracker = tracksOverlays(parentApi) ? parentApi : undefined;
|
||||
const services = {
|
||||
...coreStart,
|
||||
...pluginStart,
|
||||
};
|
||||
|
||||
let hasChanged = false;
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const cancelChanges = () => {
|
||||
// Reset to initialState in case user has changed the preview state
|
||||
if (hasChanged && fieldStatsControlsApi && initialState) {
|
||||
fieldStatsControlsApi.updateUserInput(initialState);
|
||||
}
|
||||
|
||||
flyoutSession.close();
|
||||
overlayTracker?.clearOverlays();
|
||||
};
|
||||
|
||||
const update = async (nextUpdate: FieldStatsInitialState) => {
|
||||
const esqlQuery = nextUpdate?.query?.esql;
|
||||
if (isDefined(esqlQuery)) {
|
||||
const indexPatternFromQuery = getIndexPatternFromESQLQuery(esqlQuery);
|
||||
const dv = await getOrCreateDataViewByIndexPattern(
|
||||
pluginStart.data.dataViews,
|
||||
indexPatternFromQuery,
|
||||
undefined
|
||||
);
|
||||
if (dv?.id && nextUpdate.dataViewId !== dv.id) {
|
||||
nextUpdate.dataViewId = dv.id;
|
||||
}
|
||||
}
|
||||
|
||||
resolve(nextUpdate);
|
||||
flyoutSession.close();
|
||||
overlayTracker?.clearOverlays();
|
||||
};
|
||||
|
||||
const flyoutSession = overlays.openFlyout(
|
||||
toMountPoint(
|
||||
<KibanaContextProvider services={services}>
|
||||
<FieldStatisticsInitializer
|
||||
initialInput={initialState}
|
||||
onPreview={async (nextUpdate) => {
|
||||
if (fieldStatsControlsApi) {
|
||||
fieldStatsControlsApi.updateUserInput(nextUpdate);
|
||||
hasChanged = true;
|
||||
}
|
||||
}}
|
||||
onCreate={update}
|
||||
onCancel={cancelChanges}
|
||||
isNewPanel={isNewPanel}
|
||||
/>
|
||||
</KibanaContextProvider>,
|
||||
coreStart
|
||||
),
|
||||
{
|
||||
ownFocus: true,
|
||||
size: 's',
|
||||
paddingSize: 'm',
|
||||
hideCloseButton: true,
|
||||
type: 'push',
|
||||
'data-test-subj': 'fieldStatisticsInitializerFlyout',
|
||||
onClose: cancelChanges,
|
||||
}
|
||||
);
|
||||
|
||||
if (tracksOverlays(parentApi)) {
|
||||
parentApi.openOverlay(flyoutSession, { focusedPanelId });
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 type { DefaultEmbeddableApi } from '@kbn/embeddable-plugin/public';
|
||||
import type { AggregateQuery } from '@kbn/es-query';
|
||||
import type {
|
||||
HasEditCapabilities,
|
||||
PublishesBlockingError,
|
||||
PublishesDataLoading,
|
||||
PublishesDataViews,
|
||||
PublishesTimeRange,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import type { BehaviorSubject } from 'rxjs';
|
||||
import type {
|
||||
FieldStatisticsTableEmbeddableState,
|
||||
FieldStatsInitializerViewType,
|
||||
FieldStatsInitialState,
|
||||
} from '../grid_embeddable/types';
|
||||
|
||||
export interface FieldStatsControlsApi {
|
||||
viewType$: BehaviorSubject<FieldStatsInitializerViewType>;
|
||||
dataViewId$: BehaviorSubject<string>;
|
||||
query$: BehaviorSubject<AggregateQuery>;
|
||||
showDistributions$: BehaviorSubject<boolean>;
|
||||
updateUserInput: (update: Partial<FieldStatsInitialState>) => void;
|
||||
}
|
||||
|
||||
export type FieldStatisticsTableEmbeddableApi =
|
||||
DefaultEmbeddableApi<FieldStatisticsTableEmbeddableState> &
|
||||
HasEditCapabilities &
|
||||
PublishesDataViews &
|
||||
PublishesTimeRange &
|
||||
PublishesDataLoading &
|
||||
PublishesBlockingError &
|
||||
FieldStatsControlsApi;
|
|
@ -23,7 +23,7 @@ const restorableDefaults = getDefaultESQLDataVisualizerListState();
|
|||
|
||||
const EmbeddableESQLFieldStatsTableWrapper = React.memo(
|
||||
(props: ESQLDataVisualizerGridEmbeddableState) => {
|
||||
const { onTableUpdate } = props;
|
||||
const { onTableUpdate, onRenderComplete } = props;
|
||||
const [dataVisualizerListState, setDataVisualizerListState] =
|
||||
useState<Required<ESQLDataVisualizerIndexBasedAppState>>(restorableDefaults);
|
||||
|
||||
|
@ -44,15 +44,30 @@ const EmbeddableESQLFieldStatsTableWrapper = React.memo(
|
|||
overallStatsProgress,
|
||||
setLastRefresh,
|
||||
getItemIdToExpandedRowMap,
|
||||
resetData,
|
||||
} = useESQLDataVisualizerData(props, dataVisualizerListState);
|
||||
|
||||
useEffect(() => {
|
||||
setLastRefresh(Date.now());
|
||||
}, [props?.lastReloadRequestTime, setLastRefresh]);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = props.resetData$?.subscribe(() => {
|
||||
resetData();
|
||||
});
|
||||
return () => subscription?.unsubscribe();
|
||||
}, [props.resetData$, resetData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (progress === 100 && onRenderComplete) {
|
||||
onRenderComplete();
|
||||
}
|
||||
}, [progress, onRenderComplete]);
|
||||
|
||||
if (progress === 100 && configs.length === 0) {
|
||||
return <EmbeddableNoResultsEmptyPrompt />;
|
||||
}
|
||||
|
||||
return (
|
||||
<DataVisualizerTable<FieldVisConfig>
|
||||
items={configs}
|
||||
|
|
|
@ -23,7 +23,7 @@ const restorableDefaults = getDefaultDataVisualizerListState();
|
|||
const EmbeddableFieldStatsTableWrapper = (
|
||||
props: Required<FieldStatisticTableEmbeddableProps, 'dataView'>
|
||||
) => {
|
||||
const { onTableUpdate, onAddFilter } = props;
|
||||
const { onTableUpdate, onAddFilter, onRenderComplete } = props;
|
||||
|
||||
const [dataVisualizerListState, setDataVisualizerListState] =
|
||||
useState<Required<DataVisualizerIndexBasedAppState>>(restorableDefaults);
|
||||
|
@ -73,6 +73,12 @@ const EmbeddableFieldStatsTableWrapper = (
|
|||
[props.dataView, searchQueryLanguage, searchString, props.totalDocuments, onAddFilter]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (progress === 100 && onRenderComplete) {
|
||||
onRenderComplete();
|
||||
}
|
||||
}, [progress, onRenderComplete]);
|
||||
|
||||
if (progress === 100 && configs.length === 0) {
|
||||
return <EmbeddableNoResultsEmptyPrompt />;
|
||||
}
|
||||
|
|
|
@ -30,21 +30,62 @@ const EmbeddableFieldStatsTableWrapper = dynamic(() => import('./embeddable_fiel
|
|||
function isESQLFieldStatisticTableEmbeddableState(
|
||||
input: FieldStatisticTableEmbeddableProps
|
||||
): input is ESQLDataVisualizerGridEmbeddableState {
|
||||
return isPopulatedObject(input, ['esql']) && input.esql === true;
|
||||
return isPopulatedObject(input, ['isEsqlMode']) && input.isEsqlMode === true;
|
||||
}
|
||||
|
||||
function isFieldStatisticTableEmbeddableState(
|
||||
input: FieldStatisticTableEmbeddableProps
|
||||
): input is Required<FieldStatisticTableEmbeddableProps, 'dataView'> {
|
||||
return isPopulatedObject(input, ['dataView']) && Boolean(input.esql) === false;
|
||||
return isPopulatedObject(input, ['dataView']) && Boolean(input.isEsqlMode) === false;
|
||||
}
|
||||
|
||||
const FieldStatisticsWrapperContent = (props: FieldStatisticTableEmbeddableProps) => {
|
||||
if (isESQLFieldStatisticTableEmbeddableState(props)) {
|
||||
return <EmbeddableESQLFieldStatsTableWrapper {...props} />;
|
||||
return (
|
||||
<EmbeddableESQLFieldStatsTableWrapper
|
||||
dataView={props.dataView}
|
||||
esqlQuery={props.esqlQuery}
|
||||
isEsqlMode={props.isEsqlMode ?? props.esql}
|
||||
filters={props.filters}
|
||||
lastReloadRequestTime={props.lastReloadRequestTime}
|
||||
onAddFilter={props.onAddFilter}
|
||||
onTableUpdate={props.onTableUpdate}
|
||||
query={props.query}
|
||||
samplingOption={props.samplingOption}
|
||||
savedSearch={props.savedSearch}
|
||||
sessionId={props.sessionId}
|
||||
shouldGetSubfields={props.shouldGetSubfields}
|
||||
showPreviewByDefault={props.showPreviewByDefault}
|
||||
totalDocuments={props.totalDocuments}
|
||||
timeRange={props.timeRange}
|
||||
visibleFieldNames={props.visibleFieldNames}
|
||||
resetData$={props.resetData$}
|
||||
onRenderComplete={props.onRenderComplete}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (isFieldStatisticTableEmbeddableState(props)) {
|
||||
return <EmbeddableFieldStatsTableWrapper {...props} />;
|
||||
return (
|
||||
<EmbeddableFieldStatsTableWrapper
|
||||
dataView={props.dataView}
|
||||
isEsqlMode={false}
|
||||
filters={props.filters}
|
||||
lastReloadRequestTime={props.lastReloadRequestTime}
|
||||
onAddFilter={props.onAddFilter}
|
||||
onTableUpdate={props.onTableUpdate}
|
||||
query={props.query}
|
||||
samplingOption={props.samplingOption}
|
||||
savedSearch={props.savedSearch}
|
||||
sessionId={props.sessionId}
|
||||
shouldGetSubfields={props.shouldGetSubfields}
|
||||
showPreviewByDefault={props.showPreviewByDefault}
|
||||
totalDocuments={props.totalDocuments}
|
||||
timeRange={props.timeRange}
|
||||
visibleFieldNames={props.visibleFieldNames}
|
||||
resetData$={props.resetData$}
|
||||
onRenderComplete={props.onRenderComplete}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
|
@ -133,7 +174,8 @@ const FieldStatisticsWrapper = (props: FieldStatisticTableEmbeddableProps) => {
|
|||
<DatePickerContextProvider {...datePickerDeps}>
|
||||
<FieldStatisticsWrapperContent
|
||||
dataView={props.dataView}
|
||||
esql={props.esql}
|
||||
isEsqlMode={props.isEsqlMode ?? props.esql}
|
||||
esqlQuery={props.esqlQuery}
|
||||
filters={props.filters}
|
||||
lastReloadRequestTime={props.lastReloadRequestTime}
|
||||
onAddFilter={props.onAddFilter}
|
||||
|
@ -146,6 +188,9 @@ const FieldStatisticsWrapper = (props: FieldStatisticTableEmbeddableProps) => {
|
|||
showPreviewByDefault={props.showPreviewByDefault}
|
||||
totalDocuments={props.totalDocuments}
|
||||
visibleFieldNames={props.visibleFieldNames}
|
||||
resetData$={props.resetData$}
|
||||
timeRange={props.timeRange}
|
||||
onRenderComplete={props.onRenderComplete}
|
||||
/>
|
||||
</DatePickerContextProvider>
|
||||
</KibanaContextProvider>
|
|
@ -5,13 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { AggregateQuery, Filter } from '@kbn/es-query';
|
||||
import type { AggregateQuery, Filter, TimeRange } from '@kbn/es-query';
|
||||
import type { Query } from '@kbn/es-query';
|
||||
import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
|
||||
import type { SavedSearch } from '@kbn/saved-search-plugin/public';
|
||||
import type { BehaviorSubject } from 'rxjs';
|
||||
import type { DefaultEmbeddableApi } from '@kbn/embeddable-plugin/public';
|
||||
import type { SerializedTitles } from '@kbn/presentation-publishing';
|
||||
import type { BehaviorSubject, Observable } from 'rxjs';
|
||||
import type { SerializedTimeRange, SerializedTitles } from '@kbn/presentation-publishing';
|
||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import type { DataVisualizerTableState } from '../../../../../common/types';
|
||||
import type { SamplingOption } from '../../../../../common/types/field_stats';
|
||||
|
@ -82,6 +81,8 @@ export interface FieldStatisticTableEmbeddableProps {
|
|||
* If esql:true, switch table to ES|QL mode
|
||||
*/
|
||||
esql?: boolean;
|
||||
isEsqlMode?: boolean;
|
||||
esqlQuery?: AggregateQuery;
|
||||
/**
|
||||
* If esql:true, the index pattern is used to validate time field
|
||||
*/
|
||||
|
@ -98,6 +99,9 @@ export interface FieldStatisticTableEmbeddableProps {
|
|||
*/
|
||||
overridableServices?: { data: DataPublicPluginStart };
|
||||
renderFieldName?: (fieldName: string, item: DataVisualizerTableItem) => JSX.Element;
|
||||
resetData$?: Observable<number>;
|
||||
timeRange?: TimeRange;
|
||||
onRenderComplete?: () => void;
|
||||
}
|
||||
|
||||
export type ESQLDataVisualizerGridEmbeddableState = Omit<
|
||||
|
@ -105,12 +109,21 @@ export type ESQLDataVisualizerGridEmbeddableState = Omit<
|
|||
'query'
|
||||
> & { query?: ESQLQuery };
|
||||
|
||||
export type FieldStatisticsTableEmbeddableState = FieldStatisticTableEmbeddableProps &
|
||||
SerializedTitles;
|
||||
interface FieldStatisticsTableEmbeddableComponentApi {
|
||||
showDistributions$?: BehaviorSubject<boolean>;
|
||||
export enum FieldStatsInitializerViewType {
|
||||
DATA_VIEW = 'dataview',
|
||||
ESQL = 'esql',
|
||||
}
|
||||
|
||||
export interface FieldStatsInitialState {
|
||||
dataViewId?: string;
|
||||
viewType?: FieldStatsInitializerViewType;
|
||||
query?: AggregateQuery;
|
||||
showDistributions?: boolean;
|
||||
}
|
||||
export type FieldStatisticsTableEmbeddableState = FieldStatsInitialState &
|
||||
SerializedTitles &
|
||||
SerializedTimeRange & {};
|
||||
|
||||
export type OnAddFilter = (field: DataViewField | string, value: string, type: '+' | '-') => void;
|
||||
export interface FieldStatisticsTableEmbeddableParentApi {
|
||||
executionContext?: { value: string };
|
||||
|
@ -119,10 +132,6 @@ export interface FieldStatisticsTableEmbeddableParentApi {
|
|||
onAddFilter?: OnAddFilter;
|
||||
}
|
||||
|
||||
export type FieldStatisticsTableEmbeddableApi =
|
||||
DefaultEmbeddableApi<FieldStatisticsTableEmbeddableState> &
|
||||
FieldStatisticsTableEmbeddableComponentApi;
|
||||
|
||||
export type DataVisualizerGridEmbeddableApi = Partial<FieldStatisticsTableEmbeddableState>;
|
||||
|
||||
export type ESQLDefaultLimitSizeOption = '5000' | '10000' | '100000';
|
||||
|
|
|
@ -20,6 +20,7 @@ import { useExecutionContext } from '@kbn/kibana-react-plugin/public';
|
|||
import type { AggregateQuery, Query } from '@kbn/es-query';
|
||||
import { useTimeBuckets } from '@kbn/ml-time-buckets';
|
||||
import { buildEsQuery } from '@kbn/es-query';
|
||||
import usePrevious from 'react-use/lib/usePrevious';
|
||||
import type { FieldVisConfig } from '../../../../../common/types/field_vis_config';
|
||||
import type { SupportedFieldType } from '../../../../../common/types/job_field_type';
|
||||
import type { ItemIdToExpandedRowMap } from '../../../common/components/stats_table';
|
||||
|
@ -82,7 +83,7 @@ export const useESQLDataVisualizerData = (
|
|||
) => {
|
||||
const [lastRefresh, setLastRefresh] = useState(0);
|
||||
const { services } = useDataVisualizerKibana();
|
||||
const { uiSettings, fieldFormats, executionContext } = services;
|
||||
const { uiSettings, executionContext, data } = services;
|
||||
|
||||
const parentExecutionContext = useObservable(executionContext?.context$);
|
||||
|
||||
|
@ -107,17 +108,20 @@ export const useESQLDataVisualizerData = (
|
|||
autoRefreshSelector: true,
|
||||
});
|
||||
|
||||
const [delayedESQLQuery, setDelayedESQLQuery] = useState<ESQLQuery | undefined>(input?.esqlQuery);
|
||||
const previousQuery = usePrevious(delayedESQLQuery);
|
||||
|
||||
const { currentDataView, parentQuery, parentFilters, query, visibleFieldNames, indexPattern } =
|
||||
useMemo(() => {
|
||||
let q = FALLBACK_ESQL_QUERY;
|
||||
|
||||
if (input?.query && isESQLQuery(input?.query)) q = input.query;
|
||||
if (delayedESQLQuery && isESQLQuery(delayedESQLQuery)) q = delayedESQLQuery;
|
||||
if (input?.savedSearch && isESQLQuery(input.savedSearch.searchSource.getField('query'))) {
|
||||
q = input.savedSearch.searchSource.getField('query') as ESQLQuery;
|
||||
}
|
||||
return {
|
||||
currentDataView: input.dataView,
|
||||
query: q ?? FALLBACK_ESQL_QUERY,
|
||||
query: q,
|
||||
// It's possible that in a dashboard setting, we will have additional filters and queries
|
||||
parentQuery: input?.query,
|
||||
parentFilters: input?.filters,
|
||||
|
@ -131,6 +135,7 @@ export const useESQLDataVisualizerData = (
|
|||
input?.filters,
|
||||
input?.visibleFieldNames,
|
||||
input?.indexPattern,
|
||||
delayedESQLQuery,
|
||||
]);
|
||||
|
||||
const restorableDefaults = useMemo(
|
||||
|
@ -181,6 +186,7 @@ export const useESQLDataVisualizerData = (
|
|||
(Array.isArray(parentQuery) ? parentQuery : [parentQuery]) as AnyQuery | AnyQuery[],
|
||||
parentFilters ?? []
|
||||
);
|
||||
const timeRange = input.timeRange ? input.timeRange : timefilter.getTime();
|
||||
|
||||
if (currentDataView?.timeFieldName) {
|
||||
if (Array.isArray(filter?.bool?.filter)) {
|
||||
|
@ -188,8 +194,8 @@ export const useESQLDataVisualizerData = (
|
|||
range: {
|
||||
[currentDataView.timeFieldName]: {
|
||||
format: 'strict_date_optional_time',
|
||||
gte: timefilter.getTime().from,
|
||||
lte: timefilter.getTime().to,
|
||||
gte: timeRange.from,
|
||||
lte: timeRange.to,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -202,8 +208,8 @@ export const useESQLDataVisualizerData = (
|
|||
range: {
|
||||
[currentDataView.timeFieldName]: {
|
||||
format: 'strict_date_optional_time',
|
||||
gte: timefilter.getTime().from,
|
||||
lte: timefilter.getTime().to,
|
||||
gte: timeRange.from,
|
||||
lte: timeRange.to,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -239,6 +245,8 @@ export const useESQLDataVisualizerData = (
|
|||
indexPattern,
|
||||
lastRefresh,
|
||||
limitSize,
|
||||
input.timeRange?.from,
|
||||
input.timeRange?.to,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -420,7 +428,7 @@ export const useESQLDataVisualizerData = (
|
|||
...field,
|
||||
...fieldData,
|
||||
loading: fieldData?.existsInDocs ?? true,
|
||||
fieldFormat: fieldFormats.deserialize({ id: field.secondaryType }),
|
||||
fieldFormat: data.fieldFormats.deserialize({ id: field.secondaryType }),
|
||||
aggregatable: true,
|
||||
deletable: false,
|
||||
type: getFieldType(field) as SupportedFieldType,
|
||||
|
@ -493,7 +501,7 @@ export const useESQLDataVisualizerData = (
|
|||
secondaryType: getFieldType(field) as SupportedFieldType,
|
||||
loading: fieldData?.existsInDocs ?? true,
|
||||
deletable: false,
|
||||
fieldFormat: fieldFormats.deserialize({ id: field.secondaryType }),
|
||||
fieldFormat: data.fieldFormats.deserialize({ id: field.secondaryType }),
|
||||
};
|
||||
|
||||
// Map the field type from the Kibana index pattern to the field type
|
||||
|
@ -598,12 +606,14 @@ export const useESQLDataVisualizerData = (
|
|||
totalDocuments={totalCount}
|
||||
typeAccessor="secondaryType"
|
||||
timeFieldName={timeFieldName}
|
||||
onAddFilter={input.onAddFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return map;
|
||||
}, {} as ItemIdToExpandedRowMap);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[currentDataView, totalCount, query.esql, timeFieldName]
|
||||
);
|
||||
|
||||
|
@ -633,6 +643,14 @@ export const useESQLDataVisualizerData = (
|
|||
[cancelFieldStatsRequest, cancelOverallStatsRequest]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (previousQuery?.esql !== input?.esqlQuery?.esql) {
|
||||
resetData();
|
||||
setDelayedESQLQuery(input?.esqlQuery);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [input?.esqlQuery?.esql, resetData]);
|
||||
|
||||
return {
|
||||
totalCount,
|
||||
progress: combinedProgress,
|
||||
|
|
|
@ -49,7 +49,7 @@ import {
|
|||
getDefaultPageState,
|
||||
} from '../constants/index_data_visualizer_viewer';
|
||||
import { getFieldsWithSubFields } from '../utils/get_fields_with_subfields_utils';
|
||||
import type { FieldStatisticsTableEmbeddableState } from '../embeddables/grid_embeddable/types';
|
||||
import type { FieldStatisticTableEmbeddableProps } from '../embeddables/grid_embeddable/types';
|
||||
|
||||
const defaults = getDefaultPageState();
|
||||
|
||||
|
@ -64,7 +64,7 @@ const DEFAULT_SAMPLING_OPTION: SamplingOption = {
|
|||
};
|
||||
export const useDataVisualizerGridData = (
|
||||
// Data view is required for non-ES|QL queries like kuery or lucene
|
||||
input: Required<FieldStatisticsTableEmbeddableState, 'dataView'>,
|
||||
input: Required<FieldStatisticTableEmbeddableProps, 'dataView'>,
|
||||
dataVisualizerListState: Required<DataVisualizerIndexBasedAppState>,
|
||||
savedRandomSamplerPreference?: RandomSamplerOption,
|
||||
onUpdate?: (params: Dictionary<unknown>) => void
|
||||
|
@ -207,17 +207,19 @@ export const useDataVisualizerGridData = (
|
|||
|
||||
const tf = timefilter;
|
||||
|
||||
if (!buckets || !tf || !currentDataView) return;
|
||||
if (!buckets || !tf || !currentDataView || lastRefresh === 0) return;
|
||||
|
||||
const activeBounds = tf.getActiveBounds();
|
||||
|
||||
let earliest: number | undefined;
|
||||
let latest: number | undefined;
|
||||
let earliest: number | string | undefined;
|
||||
let latest: number | string | undefined;
|
||||
if (activeBounds !== undefined && currentDataView.timeFieldName !== undefined) {
|
||||
earliest = activeBounds.min?.valueOf();
|
||||
latest = activeBounds.max?.valueOf();
|
||||
}
|
||||
|
||||
if (input.timeRange) {
|
||||
earliest = input.timeRange.from;
|
||||
latest = input.timeRange.to;
|
||||
}
|
||||
const bounds = tf.getActiveBounds();
|
||||
const barTarget = uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET) ?? DEFAULT_BAR_TARGET;
|
||||
buckets.setInterval('auto');
|
||||
|
@ -264,23 +266,19 @@ export const useDataVisualizerGridData = (
|
|||
nonAggregatableFields,
|
||||
browserSessionSeed,
|
||||
samplingOption: { ...samplingOption, seed: browserSessionSeed.toString() },
|
||||
componentExecutionContext,
|
||||
embeddableExecutionContext: componentExecutionContext,
|
||||
};
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
_timeBuckets,
|
||||
timefilter,
|
||||
currentDataView.id,
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
JSON.stringify(searchQuery),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
JSON.stringify(samplingOption),
|
||||
searchSessionId,
|
||||
lastRefresh,
|
||||
fieldsToFetch,
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
JSON.stringify({ searchQuery, samplingOption, fieldsToFetch }),
|
||||
searchSessionId,
|
||||
browserSessionSeed,
|
||||
componentExecutionContext,
|
||||
input.timeRange?.from,
|
||||
input.timeRange?.to,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -335,7 +333,6 @@ export const useDataVisualizerGridData = (
|
|||
() => overallStatsProgress.loaded * 0.2 + strategyResponse.progress.loaded * 0.8,
|
||||
[overallStatsProgress.loaded, strategyResponse.progress.loaded]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const timeUpdateSubscription = merge(
|
||||
timefilter.getTimeUpdate$(),
|
||||
|
|
|
@ -13,7 +13,8 @@ import { last, cloneDeep } from 'lodash';
|
|||
import { mergeMap, switchMap } from 'rxjs';
|
||||
import { Comparators } from '@elastic/eui';
|
||||
import type { ISearchOptions } from '@kbn/search-types';
|
||||
import { buildBaseFilterCriteria, getSafeAggregationName } from '@kbn/ml-query-utils';
|
||||
import { getSafeAggregationName } from '@kbn/ml-query-utils';
|
||||
import { buildFilterCriteria } from '../../../../common/utils/build_query_filters';
|
||||
import type {
|
||||
DataStatsFetchProgress,
|
||||
FieldStatsSearchStrategyReturnBase,
|
||||
|
@ -146,7 +147,7 @@ export function useFieldStatsSearchStrategy(
|
|||
return;
|
||||
}
|
||||
|
||||
const filterCriteria = buildBaseFilterCriteria(
|
||||
const filterCriteria = buildFilterCriteria(
|
||||
searchStrategyParams.timeFieldName,
|
||||
searchStrategyParams.earliest,
|
||||
searchStrategyParams.latest,
|
||||
|
|
|
@ -16,7 +16,6 @@ import type {
|
|||
} from '@kbn/search-types';
|
||||
import { extractErrorProperties } from '@kbn/ml-error-utils';
|
||||
import { getProcessedFields } from '@kbn/ml-data-grid';
|
||||
import { buildBaseFilterCriteria } from '@kbn/ml-query-utils';
|
||||
import { isDefined } from '@kbn/ml-is-defined';
|
||||
import type { FieldSpec } from '@kbn/data-views-plugin/common';
|
||||
import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
|
@ -52,6 +51,7 @@ import {
|
|||
fetchDataWithTimeout,
|
||||
rateLimitingForkJoin,
|
||||
} from '../search_strategy/requests/fetch_utils';
|
||||
import { buildFilterCriteria } from '../../../../common/utils/build_query_filters';
|
||||
|
||||
const getPopulatedFieldsInIndex = (
|
||||
populatedFieldsInIndexWithoutRuntimeFields: Set<string> | undefined | null,
|
||||
|
@ -119,12 +119,7 @@ export function useOverallStats<TParams extends OverallStatsSearchStrategyParams
|
|||
return;
|
||||
}
|
||||
|
||||
const filterCriteria = buildBaseFilterCriteria(
|
||||
timeFieldName,
|
||||
earliest,
|
||||
latest,
|
||||
searchQuery
|
||||
);
|
||||
const filterCriteria = buildFilterCriteria(timeFieldName, earliest, latest, searchQuery);
|
||||
|
||||
// Getting non-empty fields for the index pattern
|
||||
// because then we can absolutely exclude these from subsequent requests
|
||||
|
|
|
@ -12,12 +12,12 @@ import type { ISearchOptions } from '@kbn/search-types';
|
|||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import seedrandom from 'seedrandom';
|
||||
import { isDefined } from '@kbn/ml-is-defined';
|
||||
import { buildBaseFilterCriteria } from '@kbn/ml-query-utils';
|
||||
import { RANDOM_SAMPLER_PROBABILITIES } from '../../constants/random_sampler';
|
||||
import type {
|
||||
DocumentCountStats,
|
||||
OverallStatsSearchStrategyParams,
|
||||
} from '../../../../../common/types/field_stats';
|
||||
import { buildFilterCriteria } from '../../../../../common/utils/build_query_filters';
|
||||
|
||||
const MINIMUM_RANDOM_SAMPLER_DOC_COUNT = 100000;
|
||||
const DEFAULT_INITIAL_RANDOM_SAMPLER_PROBABILITY = 0.000001;
|
||||
|
@ -45,7 +45,7 @@ export const getDocumentCountStats = async (
|
|||
// Probability = 1 represents no sampling
|
||||
const result = { randomlySampled: false, took: 0, totalCount: 0, probability: 1 };
|
||||
|
||||
const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, searchQuery);
|
||||
const filterCriteria = buildFilterCriteria(timeFieldName, earliestMs, latestMs, searchQuery);
|
||||
|
||||
const query = {
|
||||
bool: {
|
||||
|
@ -229,12 +229,11 @@ export const processDocumentCountStats = (
|
|||
buckets[time] = dataForTime.doc_count;
|
||||
totalCount += dataForTime.doc_count;
|
||||
});
|
||||
|
||||
return {
|
||||
interval: params.intervalMs,
|
||||
buckets,
|
||||
timeRangeEarliest: params.earliest,
|
||||
timeRangeLatest: params.latest,
|
||||
timeRangeEarliest: typeof params.earliest === 'number' ? params.earliest : undefined,
|
||||
timeRangeLatest: typeof params.latest === 'number' ? params.latest : undefined,
|
||||
totalCount,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -16,7 +16,6 @@ import type {
|
|||
import type { ISearchStart } from '@kbn/data-plugin/public';
|
||||
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
|
||||
import type { SearchHit } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { buildBaseFilterCriteria } from '@kbn/ml-query-utils';
|
||||
import { extractErrorProperties } from '@kbn/ml-error-utils';
|
||||
import { getUniqGeoOrStrExamples } from '../../../common/util/example_utils';
|
||||
import type {
|
||||
|
@ -27,6 +26,7 @@ import type {
|
|||
} from '../../../../../common/types/field_stats';
|
||||
import { isIKibanaSearchResponse } from '../../../../../common/types/field_stats';
|
||||
import { MAX_EXAMPLES_DEFAULT } from './constants';
|
||||
import { buildFilterCriteria } from '../../../../../common/utils/build_query_filters';
|
||||
|
||||
export const getFieldExamplesRequest = (params: FieldStatsCommonRequestParams, field: Field) => {
|
||||
const { index, timeFieldName, earliestMs, latestMs, query, runtimeFieldMap, maxExamples } =
|
||||
|
@ -35,7 +35,7 @@ export const getFieldExamplesRequest = (params: FieldStatsCommonRequestParams, f
|
|||
// Request at least 100 docs so that we have a chance of obtaining
|
||||
// 'maxExamples' of the field.
|
||||
const size = Math.max(100, maxExamples ?? MAX_EXAMPLES_DEFAULT);
|
||||
const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, query);
|
||||
const filterCriteria = buildFilterCriteria(timeFieldName, earliestMs, latestMs, query);
|
||||
|
||||
// Use an exists filter to return examples of the field.
|
||||
if (Array.isArray(filterCriteria)) {
|
||||
|
|
|
@ -11,7 +11,7 @@ import type { Query } from '@kbn/es-query';
|
|||
import type { IKibanaSearchResponse } from '@kbn/search-types';
|
||||
import type { AggCardinality } from '@kbn/ml-agg-utils';
|
||||
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
|
||||
import { buildBaseFilterCriteria, getSafeAggregationName } from '@kbn/ml-query-utils';
|
||||
import { getSafeAggregationName } from '@kbn/ml-query-utils';
|
||||
import { buildAggregationWithSamplingOption } from './build_random_sampler_agg';
|
||||
import { getDatafeedAggregations } from '../../../../../common/utils/datafeed_utils';
|
||||
import type { AggregatableField, NonAggregatableField } from '../../types/overall_stats';
|
||||
|
@ -20,6 +20,7 @@ import type {
|
|||
OverallStatsSearchStrategyParams,
|
||||
SamplingOption,
|
||||
} from '../../../../../common/types/field_stats';
|
||||
import { buildFilterCriteria } from '../../../../../common/utils/build_query_filters';
|
||||
|
||||
export const checkAggregatableFieldsExistRequest = (
|
||||
dataViewTitle: string,
|
||||
|
@ -27,14 +28,14 @@ export const checkAggregatableFieldsExistRequest = (
|
|||
aggregatableFields: OverallStatsSearchStrategyParams['aggregatableFields'],
|
||||
samplingOption: SamplingOption,
|
||||
timeFieldName: string | undefined,
|
||||
earliestMs?: number,
|
||||
latestMs?: number,
|
||||
earliestMs?: number | string,
|
||||
latestMs?: number | string,
|
||||
datafeedConfig?: estypes.MlDatafeed,
|
||||
runtimeMappings?: estypes.MappingRuntimeFields
|
||||
): estypes.SearchRequest => {
|
||||
const index = dataViewTitle;
|
||||
const size = 0;
|
||||
const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, query);
|
||||
const filterCriteria = buildFilterCriteria(timeFieldName, earliestMs, latestMs, query);
|
||||
const datafeedAggregations = getDatafeedAggregations(datafeedConfig);
|
||||
|
||||
// Value count aggregation faster way of checking if field exists than using
|
||||
|
@ -217,13 +218,13 @@ export const checkNonAggregatableFieldExistsRequest = (
|
|||
query: Query['query'],
|
||||
field: string,
|
||||
timeFieldName: string | undefined,
|
||||
earliestMs: number | undefined,
|
||||
latestMs: number | undefined,
|
||||
earliestMs: number | string | undefined,
|
||||
latestMs: number | string | undefined,
|
||||
runtimeMappings?: estypes.MappingRuntimeFields
|
||||
): estypes.SearchRequest => {
|
||||
const index = dataViewTitle;
|
||||
const size = 0;
|
||||
const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, query);
|
||||
const filterCriteria = buildFilterCriteria(timeFieldName, earliestMs, latestMs, query);
|
||||
|
||||
if (Array.isArray(filterCriteria)) {
|
||||
filterCriteria.push({ exists: { field } });
|
||||
|
@ -256,12 +257,12 @@ export const getSampleOfDocumentsForNonAggregatableFields = (
|
|||
dataViewTitle: string,
|
||||
query: Query['query'],
|
||||
timeFieldName: string | undefined,
|
||||
earliestMs: number | undefined,
|
||||
latestMs: number | undefined,
|
||||
earliestMs: number | string | undefined,
|
||||
latestMs: number | string | undefined,
|
||||
runtimeMappings?: estypes.MappingRuntimeFields
|
||||
): estypes.SearchRequest => {
|
||||
const index = dataViewTitle;
|
||||
const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, query);
|
||||
const filterCriteria = buildFilterCriteria(timeFieldName, earliestMs, latestMs, query);
|
||||
|
||||
return {
|
||||
index,
|
||||
|
|
|
@ -0,0 +1,187 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { PresentationContainer } from '@kbn/presentation-containers';
|
||||
import { tracksOverlays } from '@kbn/presentation-containers';
|
||||
import type { EmbeddableApiContext } from '@kbn/presentation-publishing';
|
||||
import type { UiActionsActionDefinition } from '@kbn/ui-actions-plugin/public';
|
||||
import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
|
||||
import type { CoreStart } from '@kbn/core-lifecycle-browser';
|
||||
import { getIndexPatternFromESQLQuery } from '@kbn/esql-utils';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import React from 'react';
|
||||
import { isDefined } from '@kbn/ml-is-defined';
|
||||
import { COMMON_VISUALIZATION_GROUPING } from '@kbn/visualizations-plugin/public';
|
||||
import { FIELD_STATS_EMBEDDABLE_TYPE } from '../embeddables/field_stats/constants';
|
||||
import type { DataVisualizerStartDependencies } from '../../common/types/data_visualizer_plugin';
|
||||
import type {
|
||||
FieldStatisticsTableEmbeddableApi,
|
||||
FieldStatsControlsApi,
|
||||
} from '../embeddables/field_stats/types';
|
||||
import { FieldStatsInitializerViewType } from '../embeddables/grid_embeddable/types';
|
||||
import type { FieldStatsInitialState } from '../embeddables/grid_embeddable/types';
|
||||
import { getOrCreateDataViewByIndexPattern } from '../search_strategy/requests/get_data_view_by_index_pattern';
|
||||
import { FieldStatisticsInitializer } from '../embeddables/field_stats/field_stats_initializer';
|
||||
|
||||
const parentApiIsCompatible = async (
|
||||
parentApi: unknown
|
||||
): Promise<PresentationContainer | undefined> => {
|
||||
const { apiIsPresentationContainer } = await import('@kbn/presentation-containers');
|
||||
// we cannot have an async type check, so return the casted parentApi rather than a boolean
|
||||
return apiIsPresentationContainer(parentApi) ? (parentApi as PresentationContainer) : undefined;
|
||||
};
|
||||
|
||||
interface FieldStatsActionContext extends EmbeddableApiContext {
|
||||
embeddable: FieldStatisticsTableEmbeddableApi;
|
||||
}
|
||||
|
||||
async function updatePanelFromFlyoutEdits({
|
||||
api,
|
||||
isNewPanel,
|
||||
deletePanel,
|
||||
coreStart,
|
||||
pluginStart,
|
||||
initialState,
|
||||
}: {
|
||||
api: FieldStatisticsTableEmbeddableApi;
|
||||
isNewPanel: boolean;
|
||||
deletePanel?: () => void;
|
||||
coreStart: CoreStart;
|
||||
pluginStart: DataVisualizerStartDependencies;
|
||||
initialState: FieldStatsInitialState;
|
||||
fieldStatsControlsApi?: FieldStatsControlsApi;
|
||||
}) {
|
||||
const parentApi = api.parentApi;
|
||||
const overlayTracker = tracksOverlays(parentApi) ? parentApi : undefined;
|
||||
const services = {
|
||||
...coreStart,
|
||||
...pluginStart,
|
||||
};
|
||||
let hasChanged = false;
|
||||
const cancelChanges = () => {
|
||||
// Reset to initialState in case user has changed the preview state
|
||||
if (hasChanged && api && initialState) {
|
||||
api.updateUserInput(initialState);
|
||||
}
|
||||
|
||||
if (isNewPanel && deletePanel) {
|
||||
deletePanel();
|
||||
}
|
||||
flyoutSession.close();
|
||||
overlayTracker?.clearOverlays();
|
||||
};
|
||||
|
||||
const update = async (nextUpdate: FieldStatsInitialState) => {
|
||||
const esqlQuery = nextUpdate?.query?.esql;
|
||||
if (isDefined(esqlQuery)) {
|
||||
const indexPatternFromQuery = getIndexPatternFromESQLQuery(esqlQuery);
|
||||
const dv = await getOrCreateDataViewByIndexPattern(
|
||||
pluginStart.data.dataViews,
|
||||
indexPatternFromQuery,
|
||||
undefined
|
||||
);
|
||||
if (dv?.id && nextUpdate.dataViewId !== dv.id) {
|
||||
nextUpdate.dataViewId = dv.id;
|
||||
}
|
||||
}
|
||||
if (api) {
|
||||
api.updateUserInput(nextUpdate);
|
||||
}
|
||||
|
||||
flyoutSession.close();
|
||||
overlayTracker?.clearOverlays();
|
||||
};
|
||||
const flyoutSession = services.overlays.openFlyout(
|
||||
toMountPoint(
|
||||
<KibanaContextProvider services={services}>
|
||||
<FieldStatisticsInitializer
|
||||
initialInput={initialState}
|
||||
onPreview={async (nextUpdate) => {
|
||||
if (api.updateUserInput) {
|
||||
api.updateUserInput(nextUpdate);
|
||||
hasChanged = true;
|
||||
}
|
||||
}}
|
||||
onCreate={update}
|
||||
onCancel={cancelChanges}
|
||||
isNewPanel={isNewPanel}
|
||||
/>
|
||||
</KibanaContextProvider>,
|
||||
coreStart
|
||||
),
|
||||
{
|
||||
ownFocus: true,
|
||||
size: 's',
|
||||
paddingSize: 'm',
|
||||
hideCloseButton: true,
|
||||
type: 'push',
|
||||
'data-test-subj': 'fieldStatisticsInitializerFlyout',
|
||||
onClose: cancelChanges,
|
||||
}
|
||||
);
|
||||
overlayTracker?.openOverlay(flyoutSession, { focusedPanelId: api.uuid });
|
||||
}
|
||||
|
||||
export function createAddFieldStatsTableAction(
|
||||
coreStart: CoreStart,
|
||||
pluginStart: DataVisualizerStartDependencies
|
||||
): UiActionsActionDefinition<FieldStatsActionContext> {
|
||||
return {
|
||||
id: 'create-field-stats-table',
|
||||
grouping: COMMON_VISUALIZATION_GROUPING,
|
||||
order: 10,
|
||||
getIconType: () => 'inspect',
|
||||
getDisplayName: () =>
|
||||
i18n.translate('xpack.dataVisualizer.fieldStatistics.displayName', {
|
||||
defaultMessage: 'Field statistics',
|
||||
}),
|
||||
async isCompatible(context: EmbeddableApiContext) {
|
||||
return Boolean(await parentApiIsCompatible(context.embeddable));
|
||||
},
|
||||
async execute(context) {
|
||||
const presentationContainerParent = await parentApiIsCompatible(context.embeddable);
|
||||
if (!presentationContainerParent) throw new IncompatibleActionError();
|
||||
|
||||
try {
|
||||
const defaultIndexPattern = await pluginStart.data.dataViews.getDefault();
|
||||
const defaultInitialState: FieldStatsInitialState = {
|
||||
viewType: FieldStatsInitializerViewType.ESQL,
|
||||
query: {
|
||||
// Initial default query
|
||||
esql: `from ${defaultIndexPattern?.getIndexPattern()} | limit 10`,
|
||||
},
|
||||
};
|
||||
const embeddable = await presentationContainerParent.addNewPanel<
|
||||
object,
|
||||
FieldStatisticsTableEmbeddableApi
|
||||
>({
|
||||
panelType: FIELD_STATS_EMBEDDABLE_TYPE,
|
||||
initialState: defaultInitialState,
|
||||
});
|
||||
// open the flyout if embeddable has been created successfully
|
||||
if (embeddable) {
|
||||
const deletePanel = () => {
|
||||
presentationContainerParent.removePanel(embeddable.uuid);
|
||||
};
|
||||
|
||||
updatePanelFromFlyoutEdits({
|
||||
api: embeddable,
|
||||
isNewPanel: true,
|
||||
deletePanel,
|
||||
coreStart,
|
||||
pluginStart,
|
||||
initialState: defaultInitialState,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 type { CoreStart } from '@kbn/core-lifecycle-browser';
|
||||
import type { UiActionsSetup } from '@kbn/ui-actions-plugin/public';
|
||||
import type { DataVisualizerStartDependencies } from '../../common/types/data_visualizer_plugin';
|
||||
|
||||
export function registerDataVisualizerUiActions(
|
||||
uiActions: UiActionsSetup,
|
||||
coreStart: CoreStart,
|
||||
pluginStart: DataVisualizerStartDependencies
|
||||
) {
|
||||
import('./create_field_stats_table').then(({ createAddFieldStatsTableAction }) => {
|
||||
const addFieldStatsAction = createAddFieldStatsTableAction(coreStart, pluginStart);
|
||||
uiActions.addTriggerAction('ADD_PANEL_TRIGGER', addFieldStatsAction);
|
||||
});
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
import type { DataView, DataViewField } from '@kbn/data-views-plugin/common';
|
||||
import { isDefined } from '@kbn/ml-is-defined';
|
||||
import type { FieldStatisticsTableEmbeddableState } from '../embeddables/grid_embeddable/types';
|
||||
import type { FieldStatisticTableEmbeddableProps } from '../embeddables/grid_embeddable/types';
|
||||
|
||||
/**
|
||||
* Helper logic to add multi-fields to the table for embeddables outside of Index data visualizer
|
||||
|
@ -19,7 +19,7 @@ export const getFieldsWithSubFields = ({
|
|||
currentDataView,
|
||||
shouldGetSubfields = false,
|
||||
}: {
|
||||
input: FieldStatisticsTableEmbeddableState;
|
||||
input: FieldStatisticTableEmbeddableProps;
|
||||
currentDataView: DataView;
|
||||
shouldGetSubfields: boolean;
|
||||
}) => {
|
||||
|
|
|
@ -20,6 +20,8 @@ import type {
|
|||
DataVisualizerSetupDependencies,
|
||||
DataVisualizerStartDependencies,
|
||||
} from './application/common/types/data_visualizer_plugin';
|
||||
import { registerEmbeddables } from './application/index_data_visualizer/embeddables/field_stats';
|
||||
import { registerDataVisualizerUiActions } from './application/index_data_visualizer/ui_actions';
|
||||
export type DataVisualizerPluginSetup = ReturnType<DataVisualizerPlugin['setup']>;
|
||||
export type DataVisualizerPluginStart = ReturnType<DataVisualizerPlugin['start']>;
|
||||
|
||||
|
@ -50,7 +52,17 @@ export class DataVisualizerPlugin
|
|||
}
|
||||
}
|
||||
|
||||
public setup(core: DataVisualizerCoreSetup, plugins: DataVisualizerSetupDependencies) {
|
||||
public async setup(core: DataVisualizerCoreSetup, plugins: DataVisualizerSetupDependencies) {
|
||||
if (plugins.embeddable) {
|
||||
registerEmbeddables(plugins.embeddable, core);
|
||||
}
|
||||
|
||||
const [coreStart, pluginStart] = await core.getStartServices();
|
||||
|
||||
if (plugins.uiActions) {
|
||||
registerDataVisualizerUiActions(plugins.uiActions, coreStart, pluginStart);
|
||||
}
|
||||
|
||||
if (plugins.home) {
|
||||
registerHomeAddData(plugins.home, this.resultsLinks);
|
||||
registerHomeFeatureCatalogue(plugins.home);
|
||||
|
@ -75,7 +87,7 @@ export class DataVisualizerPlugin
|
|||
FieldStatisticsTable: dynamic(
|
||||
async () =>
|
||||
import(
|
||||
'./application/index_data_visualizer/embeddables/grid_embeddable/field_stats_embeddable_wrapper'
|
||||
'./application/index_data_visualizer/embeddables/grid_embeddable/field_stats_wrapper'
|
||||
)
|
||||
),
|
||||
};
|
||||
|
|
|
@ -82,7 +82,12 @@
|
|||
"@kbn/presentation-publishing",
|
||||
"@kbn/shared-ux-utility",
|
||||
"@kbn/search-types",
|
||||
"@kbn/unified-field-list"
|
||||
"@kbn/unified-field-list",
|
||||
"@kbn/content-management-utils",
|
||||
"@kbn/core-lifecycle-browser",
|
||||
"@kbn/presentation-containers",
|
||||
"@kbn/react-kibana-mount",
|
||||
"@kbn/visualizations-plugin"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -44225,8 +44225,6 @@
|
|||
"uiActions.errors.incompatibleAction": "Action non compatible",
|
||||
"uiActions.triggers.rowClickkDescription": "Un clic sur une ligne de tableau",
|
||||
"uiActions.triggers.rowClickTitle": "Clic sur ligne de tableau",
|
||||
"uiActions.triggers.dashboard.addPanelMenu.description": "Une nouvelle action apparaîtra dans le menu Ajouter un panneau du tableau de bord",
|
||||
"uiActions.triggers.dashboard.addPanelMenu.title": "Menu Ajouter un panneau",
|
||||
"unsavedChangesBadge.contextMenu.openButton": "Afficher les actions disponibles",
|
||||
"unsavedChangesBadge.contextMenu.revertChangesButton": "Restaurer les modifications",
|
||||
"unsavedChangesBadge.contextMenu.revertingChangesButtonStatus": "Annuler les modifications",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue