mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[ML] Add Index data visualizer grid embeddable as extra view within Discover (#107184)
* [ML] Initial embed * [ML] Initial embed props * [ML] Add top nav link to data viz * Add visible fields * Add add data service to register links * Renames, refactor, use constants * Renames, refactor, use constants * Update tests and mocks * Embeddable * Update hook to update upon time udpate * Add filter support to query * Refactor filter utilities * Add filter support for embeddable * Fix saved search data undefined * Prototype aggregated view/document view switcher * Prototype flyout * Prototype save document view option in storage * Fix filter and query conflict with saved search * Minor styling edits * [ML] Initial embed * [ML] Initial embed props * Add embeddable 1 * Add visible fields * Embeddable 2 * Add filter support to query * Refactor filter utilities * Add filter support for embeddable * Fix saved search data undefined * Prototype aggregated view/document view switcher * Prototype flyout * Prototype save document view option in storage * Fix filter and query conflict with saved search * Minor styling edits * Fix missing code after conflicts * Remove dv locator and flyout * Make types happy * Fix types * Rename toggle option * Resolve conflicts * [ML] Reduce size of chart * [ML] Unbold name, switch icons of show distributions * [ML] Make size consistent * [ML] Make page size 25 * [ML] Switch to arrow right and down * [ML] Make legend font smaller * [ML] Add user setting * [ML] Add show preview by default setting * [ML] Match icon * Add panels around the subcontent * Add preference for aggregated vs doc * Fix types * Fix types, add constants for adv settings * Change to data view type * Temp fix for Kibana/EUI table overflow issue * Modify line height so text is not cut off, modify widths for varying screen sizes * Different width padders for different screens * Fix CI * Merge latest, move button to the right * Fix width for bar charts previews * Fix toggle buttons, fix maps * Delete unused file * Fix boolean styling * Change to enum, discover mode * Hide field stats * Hide field stats * Persist show mini preview/distribution settings * Remove window size, use size observer instead * Default to document view * Remove bold, switch icon * Set fixed width for top values, reduce font size in table * Fix custom url tests * Update width styling for panels * Fix missing flag for Discover sidebar, jest tests * Fix max width * Workaround for sorting * Fix import * Fix styling * Make height uniform, center alignment, fix map and keyword map not same size Move styling * Revert "Make height uniform, center alignment, fix map and keyword map not same size" This reverts commit8fc42e2f
* Revert "Make height uniform, center alignment, fix map and keyword map not same size" This reverts commit8fc42e2f
* Uniform height, left aligned, flex grid * Switch top values to have labels * Center content * Replace fixed widths with percentage * Fix table missing field types * Add dashboard embeddable and filter support * Fix file data viz styling and tests, lean up imports, remove hard coded pixels * Add search panel/kql filter bar * Temporarily fix scrolling * New kql filters for data visualizer * Set map height so it will fit the sampler shard size text * Use eui progress labels * Fix spacer * Add beta badge * Temporarily fix scrolling * Fix grow for Top Values for * [ML] Update functional tests to reflect new arrow icons * [ML] Add filter buttons and KQL bars * [ML] Update filter bar onChange behavior * [ML] Update top values filter onChange behavior * [ML] Update search filters when opening saved search * [ML] Clean up * [ML] Remove fit content for height * [ML] Fix boolean legend * [ML] Fix header section when browser width is small to large and when index pattern title is too large * [ML] Hide expander icon when dimension is xs or s & css fixes * [ML] Delete embeddables because they are not use * [ML] Rename view mode, refactor to separate hook, add error prompt if can't show, rename wrapper, clean up & fix tests * [ML] Make doc count 0 for empty fields, update t/f test * [ML] Add unit testing for search utils * Fix missing unsubscribe for embeddable output * Remove redundant onAddFilter for this PR, fix width * Rename Field Stats to Field stats to match convention * [ML] Fix expand all/collapse all behavior to override individual setting * [ML] Fix functional tests should be 0/0% * [ML] Fix docs content spacing, rename classnames, add filters to Discover, lens, and maps * [ML] Fix doc count for fields that exists but have no stats * [ML] Fix icon styling to match Discover but have text/keyword/histogram * [ML] Fix doc count for fields that exists but have no stats * [ML] Rename classnames to BEM style * Resolve latest changes * [ML] Add tests for data viz in Discover * Update tests & dashboard behavior to reflect new advanced settings * Update telemetry * Remove workaround after eui bump fix * Fix missing bool clause * Add login * Fix saved search attributes broken with latest changes * Update tests * Fix import * Match the no results found * Fix query util to return search source's results right away. Fix texts. * Rename old test suits to farequoteDataViewTestData Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
7f83ec09d6
commit
6600f1ad78
64 changed files with 2470 additions and 475 deletions
|
@ -19,5 +19,6 @@ export const DOC_TABLE_LEGACY = 'doc_table:legacy';
|
|||
export const MODIFY_COLUMNS_ON_SWITCH = 'discover:modifyColumnsOnSwitch';
|
||||
export const SEARCH_FIELDS_FROM_SOURCE = 'discover:searchFieldsFromSource';
|
||||
export const MAX_DOC_FIELDS_DISPLAYED = 'discover:maxDocFieldsDisplayed';
|
||||
export const SHOW_FIELD_STATISTICS = 'discover:showFieldStatistics';
|
||||
export const SHOW_MULTIFIELDS = 'discover:showMultiFields';
|
||||
export const SEARCH_EMBEDDABLE_TYPE = 'search';
|
||||
|
|
|
@ -19,6 +19,7 @@ import { discoverServiceMock } from '../../../../../__mocks__/services';
|
|||
import { FetchStatus } from '../../../../types';
|
||||
import { Chart } from './point_series';
|
||||
import { DiscoverChart } from './discover_chart';
|
||||
import { VIEW_MODE } from '../view_mode_toggle';
|
||||
|
||||
setHeaderActionMenuMounter(jest.fn());
|
||||
|
||||
|
@ -94,6 +95,8 @@ function getProps(timefield?: string) {
|
|||
state: { columns: [] },
|
||||
stateContainer: {} as GetStateReturn,
|
||||
timefield,
|
||||
viewMode: VIEW_MODE.DOCUMENT_LEVEL,
|
||||
setDiscoverViewMode: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,8 @@ import { DiscoverHistogram } from './histogram';
|
|||
import { DataCharts$, DataTotalHits$ } from '../../services/use_saved_search';
|
||||
import { DiscoverServices } from '../../../../../build_services';
|
||||
import { useChartPanels } from './use_chart_panels';
|
||||
import { VIEW_MODE, DocumentViewModeToggle } from '../view_mode_toggle';
|
||||
import { SHOW_FIELD_STATISTICS } from '../../../../../../common';
|
||||
|
||||
const DiscoverHistogramMemoized = memo(DiscoverHistogram);
|
||||
export const CHART_HIDDEN_KEY = 'discover:chartHidden';
|
||||
|
@ -36,6 +38,8 @@ export function DiscoverChart({
|
|||
state,
|
||||
stateContainer,
|
||||
timefield,
|
||||
viewMode,
|
||||
setDiscoverViewMode,
|
||||
}: {
|
||||
resetSavedSearch: () => void;
|
||||
savedSearch: SavedSearch;
|
||||
|
@ -45,8 +49,11 @@ export function DiscoverChart({
|
|||
state: AppState;
|
||||
stateContainer: GetStateReturn;
|
||||
timefield?: string;
|
||||
viewMode: VIEW_MODE;
|
||||
setDiscoverViewMode: (viewMode: VIEW_MODE) => void;
|
||||
}) {
|
||||
const [showChartOptionsPopover, setShowChartOptionsPopover] = useState(false);
|
||||
const showViewModeToggle = services.uiSettings.get(SHOW_FIELD_STATISTICS) ?? false;
|
||||
|
||||
const { data, storage } = services;
|
||||
|
||||
|
@ -108,6 +115,16 @@ export function DiscoverChart({
|
|||
onResetQuery={resetSavedSearch}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
{showViewModeToggle && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<DocumentViewModeToggle
|
||||
viewMode={viewMode}
|
||||
setDiscoverViewMode={setDiscoverViewMode}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
|
||||
{timefield && (
|
||||
<EuiFlexItem className="dscResultCount__toggle" grow={false}>
|
||||
<EuiPopover
|
||||
|
|
|
@ -4,6 +4,10 @@ discover-app {
|
|||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.dscAppWrapper {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.dscPage {
|
||||
@include euiBreakpoint('m', 'l', 'xl') {
|
||||
@include kibanaFullBodyHeight();
|
||||
|
|
|
@ -6,9 +6,8 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
import './discover_layout.scss';
|
||||
import React, { useState, useMemo, useCallback, useEffect, useRef } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
EuiSpacer,
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
|
@ -17,6 +16,7 @@ import {
|
|||
EuiPage,
|
||||
EuiPageBody,
|
||||
EuiPageContent,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
|
@ -26,7 +26,7 @@ import { LoadingSpinner } from '../loading_spinner/loading_spinner';
|
|||
import { esFilters, IndexPatternField } from '../../../../../../../data/public';
|
||||
import { DiscoverSidebarResponsive } from '../sidebar';
|
||||
import { DiscoverLayoutProps } from './types';
|
||||
import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../../../common';
|
||||
import { SEARCH_FIELDS_FROM_SOURCE, SHOW_FIELD_STATISTICS } from '../../../../../../common';
|
||||
import { popularizeField } from '../../../../helpers/popularize_field';
|
||||
import { DiscoverTopNav } from '../top_nav/discover_topnav';
|
||||
import { DocViewFilterFn, ElasticSearchHit } from '../../../../doc_views/doc_views_types';
|
||||
|
@ -43,6 +43,8 @@ import {
|
|||
SavedSearchURLConflictCallout,
|
||||
useSavedSearchAliasMatchRedirect,
|
||||
} from '../../../../../saved_searches';
|
||||
import { DiscoverDataVisualizerGrid } from '../../../../components/data_visualizer_grid';
|
||||
import { VIEW_MODE } from '../view_mode_toggle';
|
||||
|
||||
/**
|
||||
* Local storage key for sidebar persistence state
|
||||
|
@ -52,6 +54,7 @@ export const SIDEBAR_CLOSED_KEY = 'discover:sidebarClosed';
|
|||
const SidebarMemoized = React.memo(DiscoverSidebarResponsive);
|
||||
const TopNavMemoized = React.memo(DiscoverTopNav);
|
||||
const DiscoverChartMemoized = React.memo(DiscoverChart);
|
||||
const DataVisualizerGridMemoized = React.memo(DiscoverDataVisualizerGrid);
|
||||
|
||||
export function DiscoverLayout({
|
||||
indexPattern,
|
||||
|
@ -83,6 +86,19 @@ export function DiscoverLayout({
|
|||
const { main$, charts$, totalHits$ } = savedSearchData$;
|
||||
const [expandedDoc, setExpandedDoc] = useState<ElasticSearchHit | undefined>(undefined);
|
||||
const [inspectorSession, setInspectorSession] = useState<InspectorSession | undefined>(undefined);
|
||||
|
||||
const viewMode = useMemo(() => {
|
||||
if (uiSettings.get(SHOW_FIELD_STATISTICS) !== true) return VIEW_MODE.DOCUMENT_LEVEL;
|
||||
return state.viewMode ?? VIEW_MODE.DOCUMENT_LEVEL;
|
||||
}, [uiSettings, state.viewMode]);
|
||||
|
||||
const setDiscoverViewMode = useCallback(
|
||||
(mode: VIEW_MODE) => {
|
||||
stateContainer.setAppState({ viewMode: mode });
|
||||
},
|
||||
[stateContainer]
|
||||
);
|
||||
|
||||
const fetchCounter = useRef<number>(0);
|
||||
const dataState: DataMainMsg = useDataState(main$);
|
||||
|
||||
|
@ -213,6 +229,7 @@ export function DiscoverLayout({
|
|||
trackUiMetric={trackUiMetric}
|
||||
useNewFieldsApi={useNewFieldsApi}
|
||||
onEditRuntimeField={onEditRuntimeField}
|
||||
viewMode={viewMode}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiHideFor sizes={['xs', 's']}>
|
||||
|
@ -279,22 +296,36 @@ export function DiscoverLayout({
|
|||
services={services}
|
||||
stateContainer={stateContainer}
|
||||
timefield={timeField}
|
||||
viewMode={viewMode}
|
||||
setDiscoverViewMode={setDiscoverViewMode}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiHorizontalRule margin="none" />
|
||||
|
||||
<DiscoverDocuments
|
||||
documents$={savedSearchData$.documents$}
|
||||
expandedDoc={expandedDoc}
|
||||
indexPattern={indexPattern}
|
||||
navigateTo={navigateTo}
|
||||
onAddFilter={onAddFilter as DocViewFilterFn}
|
||||
savedSearch={savedSearch}
|
||||
services={services}
|
||||
setExpandedDoc={setExpandedDoc}
|
||||
state={state}
|
||||
stateContainer={stateContainer}
|
||||
/>
|
||||
{viewMode === VIEW_MODE.DOCUMENT_LEVEL ? (
|
||||
<DiscoverDocuments
|
||||
documents$={savedSearchData$.documents$}
|
||||
expandedDoc={expandedDoc}
|
||||
indexPattern={indexPattern}
|
||||
navigateTo={navigateTo}
|
||||
onAddFilter={onAddFilter as DocViewFilterFn}
|
||||
savedSearch={savedSearch}
|
||||
services={services}
|
||||
setExpandedDoc={setExpandedDoc}
|
||||
state={state}
|
||||
stateContainer={stateContainer}
|
||||
/>
|
||||
) : (
|
||||
<DataVisualizerGridMemoized
|
||||
savedSearch={savedSearch}
|
||||
services={services}
|
||||
indexPattern={indexPattern}
|
||||
query={state.query}
|
||||
filters={state.filters}
|
||||
columns={columns}
|
||||
stateContainer={stateContainer}
|
||||
onAddFilter={onAddFilter}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
</EuiPageContent>
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiSpacer,
|
||||
EuiHorizontalRule,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { UiCounterMetricType } from '@kbn/analytics';
|
||||
|
@ -251,6 +252,11 @@ export interface DiscoverFieldProps {
|
|||
* @param fieldName name of the field to delete
|
||||
*/
|
||||
onDeleteField?: (fieldName: string) => void;
|
||||
|
||||
/**
|
||||
* Optionally show or hide field stats in the popover
|
||||
*/
|
||||
showFieldStats?: boolean;
|
||||
}
|
||||
|
||||
function DiscoverFieldComponent({
|
||||
|
@ -266,6 +272,7 @@ function DiscoverFieldComponent({
|
|||
multiFields,
|
||||
onEditField,
|
||||
onDeleteField,
|
||||
showFieldStats,
|
||||
}: DiscoverFieldProps) {
|
||||
const [infoIsOpen, setOpen] = useState(false);
|
||||
|
||||
|
@ -362,15 +369,27 @@ function DiscoverFieldComponent({
|
|||
const details = getDetails(field);
|
||||
return (
|
||||
<>
|
||||
<DiscoverFieldDetails
|
||||
indexPattern={indexPattern}
|
||||
field={field}
|
||||
details={details}
|
||||
onAddFilter={onAddFilter}
|
||||
/>
|
||||
{showFieldStats && (
|
||||
<>
|
||||
<EuiTitle size="xxxs">
|
||||
<h5>
|
||||
{i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', {
|
||||
defaultMessage: 'Top 5 values',
|
||||
})}
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
<DiscoverFieldDetails
|
||||
indexPattern={indexPattern}
|
||||
field={field}
|
||||
details={details}
|
||||
onAddFilter={onAddFilter}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{multiFields && (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
{showFieldStats && <EuiSpacer size="m" />}
|
||||
<MultiFields
|
||||
multiFields={multiFields}
|
||||
alwaysShowActionButton={alwaysShowActionButton}
|
||||
|
@ -378,6 +397,7 @@ function DiscoverFieldComponent({
|
|||
/>
|
||||
</>
|
||||
)}
|
||||
{(showFieldStats || multiFields) && <EuiHorizontalRule margin="m" />}
|
||||
<DiscoverFieldVisualize
|
||||
field={field}
|
||||
indexPattern={indexPattern}
|
||||
|
@ -388,7 +408,6 @@ function DiscoverFieldComponent({
|
|||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
display="block"
|
||||
|
@ -418,13 +437,6 @@ function DiscoverFieldComponent({
|
|||
panelClassName="dscSidebarItem__fieldPopoverPanel"
|
||||
>
|
||||
{popoverTitle}
|
||||
<EuiTitle size="xxxs">
|
||||
<h5>
|
||||
{i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', {
|
||||
defaultMessage: 'Top 5 values',
|
||||
})}
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
{infoIsOpen && renderPopover()}
|
||||
</EuiPopover>
|
||||
);
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { EuiButton, EuiPopoverFooter } from '@elastic/eui';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics';
|
||||
import type { IndexPattern, IndexPatternField } from 'src/plugins/data/common';
|
||||
|
@ -46,21 +46,19 @@ export const DiscoverFieldVisualize: React.FC<Props> = React.memo(
|
|||
};
|
||||
|
||||
return (
|
||||
<EuiPopoverFooter>
|
||||
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
|
||||
<EuiButton
|
||||
fullWidth
|
||||
size="s"
|
||||
href={visualizeInfo.href}
|
||||
onClick={handleVisualizeLinkClick}
|
||||
data-test-subj={`fieldVisualize-${field.name}`}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="discover.fieldChooser.visualizeButton.label"
|
||||
defaultMessage="Visualize"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiPopoverFooter>
|
||||
// eslint-disable-next-line @elastic/eui/href-or-on-click
|
||||
<EuiButton
|
||||
fullWidth
|
||||
size="s"
|
||||
href={visualizeInfo.href}
|
||||
onClick={handleVisualizeLinkClick}
|
||||
data-test-subj={`fieldVisualize-${field.name}`}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="discover.fieldChooser.visualizeButton.label"
|
||||
defaultMessage="Visualize"
|
||||
/>
|
||||
</EuiButton>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -22,6 +22,7 @@ import { DiscoverSidebarComponent as DiscoverSidebar } from './discover_sidebar'
|
|||
import { ElasticSearchHit } from '../../../../doc_views/doc_views_types';
|
||||
import { discoverServiceMock as mockDiscoverServices } from '../../../../../__mocks__/services';
|
||||
import { stubLogstashIndexPattern } from '../../../../../../../data/common/stubs';
|
||||
import { VIEW_MODE } from '../view_mode_toggle';
|
||||
|
||||
jest.mock('../../../../../kibana_services', () => ({
|
||||
getServices: () => mockDiscoverServices,
|
||||
|
@ -65,6 +66,7 @@ function getCompProps(): DiscoverSidebarProps {
|
|||
setFieldFilter: jest.fn(),
|
||||
onEditRuntimeField: jest.fn(),
|
||||
editField: jest.fn(),
|
||||
viewMode: VIEW_MODE.DOCUMENT_LEVEL,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@ import { getIndexPatternFieldList } from './lib/get_index_pattern_field_list';
|
|||
import { DiscoverSidebarResponsiveProps } from './discover_sidebar_responsive';
|
||||
import { DiscoverIndexPatternManagement } from './discover_index_pattern_management';
|
||||
import { ElasticSearchHit } from '../../../../doc_views/doc_views_types';
|
||||
import { VIEW_MODE } from '../view_mode_toggle';
|
||||
|
||||
/**
|
||||
* Default number of available fields displayed and added on scroll
|
||||
|
@ -77,6 +78,10 @@ export interface DiscoverSidebarProps extends Omit<DiscoverSidebarResponsiveProp
|
|||
* hits fetched from ES, displayed in the doc table
|
||||
*/
|
||||
documents?: ElasticSearchHit[];
|
||||
/**
|
||||
* Discover view mode
|
||||
*/
|
||||
viewMode: VIEW_MODE;
|
||||
}
|
||||
|
||||
export function DiscoverSidebarComponent({
|
||||
|
@ -100,6 +105,7 @@ export function DiscoverSidebarComponent({
|
|||
setFieldEditorRef,
|
||||
closeFlyout,
|
||||
editField,
|
||||
viewMode,
|
||||
}: DiscoverSidebarProps) {
|
||||
const [fields, setFields] = useState<IndexPatternField[] | null>(null);
|
||||
|
||||
|
@ -205,6 +211,8 @@ export function DiscoverSidebarComponent({
|
|||
return result;
|
||||
}, [fields]);
|
||||
|
||||
const showFieldStats = useMemo(() => viewMode === VIEW_MODE.DOCUMENT_LEVEL, [viewMode]);
|
||||
|
||||
const calculateMultiFields = () => {
|
||||
if (!useNewFieldsApi || !fields) {
|
||||
return undefined;
|
||||
|
@ -407,6 +415,7 @@ export function DiscoverSidebarComponent({
|
|||
multiFields={multiFields?.get(field.name)}
|
||||
onEditField={canEditIndexPatternField ? editField : undefined}
|
||||
onDeleteField={canEditIndexPatternField ? deleteField : undefined}
|
||||
showFieldStats={showFieldStats}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
|
@ -466,6 +475,7 @@ export function DiscoverSidebarComponent({
|
|||
multiFields={multiFields?.get(field.name)}
|
||||
onEditField={canEditIndexPatternField ? editField : undefined}
|
||||
onDeleteField={canEditIndexPatternField ? deleteField : undefined}
|
||||
showFieldStats={showFieldStats}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
|
@ -494,6 +504,7 @@ export function DiscoverSidebarComponent({
|
|||
multiFields={multiFields?.get(field.name)}
|
||||
onEditField={canEditIndexPatternField ? editField : undefined}
|
||||
onDeleteField={canEditIndexPatternField ? deleteField : undefined}
|
||||
showFieldStats={showFieldStats}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
|
|
|
@ -26,6 +26,7 @@ import { ElasticSearchHit } from '../../../../doc_views/doc_views_types';
|
|||
import { FetchStatus } from '../../../../types';
|
||||
import { DataDocuments$ } from '../../services/use_saved_search';
|
||||
import { stubLogstashIndexPattern } from '../../../../../../../data/common/stubs';
|
||||
import { VIEW_MODE } from '../view_mode_toggle';
|
||||
|
||||
const mockServices = {
|
||||
history: () => ({
|
||||
|
@ -103,6 +104,7 @@ function getCompProps(): DiscoverSidebarResponsiveProps {
|
|||
state: {},
|
||||
trackUiMetric: jest.fn(),
|
||||
onEditRuntimeField: jest.fn(),
|
||||
viewMode: VIEW_MODE.DOCUMENT_LEVEL,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ import { AppState } from '../../services/discover_state';
|
|||
import { DiscoverIndexPatternManagement } from './discover_index_pattern_management';
|
||||
import { DataDocuments$ } from '../../services/use_saved_search';
|
||||
import { calcFieldCounts } from '../../utils/calc_field_counts';
|
||||
import { VIEW_MODE } from '../view_mode_toggle';
|
||||
|
||||
export interface DiscoverSidebarResponsiveProps {
|
||||
/**
|
||||
|
@ -106,6 +107,10 @@ export interface DiscoverSidebarResponsiveProps {
|
|||
* callback to execute on edit runtime field
|
||||
*/
|
||||
onEditRuntimeField: () => void;
|
||||
/**
|
||||
* Discover view mode
|
||||
*/
|
||||
viewMode: VIEW_MODE;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -16,6 +16,7 @@ import { SavedSearch } from '../../../../../saved_searches';
|
|||
import { onSaveSearch } from './on_save_search';
|
||||
import { GetStateReturn } from '../../services/discover_state';
|
||||
import { openOptionsPopover } from './open_options_popover';
|
||||
import type { TopNavMenuData } from '../../../../../../../navigation/public';
|
||||
|
||||
/**
|
||||
* Helper function to build the top nav links
|
||||
|
@ -38,7 +39,7 @@ export const getTopNavLinks = ({
|
|||
onOpenInspector: () => void;
|
||||
searchSource: ISearchSource;
|
||||
onOpenSavedSearch: (id: string) => void;
|
||||
}) => {
|
||||
}): TopNavMenuData[] => {
|
||||
const options = {
|
||||
id: 'options',
|
||||
label: i18n.translate('discover.localMenu.localMenu.optionsTitle', {
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
@import 'view_mode_toggle';
|
|
@ -0,0 +1,12 @@
|
|||
.dscViewModeToggle {
|
||||
padding-right: $euiSize;
|
||||
}
|
||||
|
||||
.fieldStatsButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.fieldStatsBetaBadge {
|
||||
margin-left: $euiSizeXS;
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export enum VIEW_MODE {
|
||||
DOCUMENT_LEVEL = 'documents',
|
||||
AGGREGATED_LEVEL = 'aggregated',
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { DocumentViewModeToggle } from './view_mode_toggle';
|
||||
export { VIEW_MODE } from './constants';
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { EuiButtonGroup, EuiBetaBadge } from '@elastic/eui';
|
||||
import React, { useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { VIEW_MODE } from './constants';
|
||||
import './_index.scss';
|
||||
|
||||
export const DocumentViewModeToggle = ({
|
||||
viewMode,
|
||||
setDiscoverViewMode,
|
||||
}: {
|
||||
viewMode: VIEW_MODE;
|
||||
setDiscoverViewMode: (viewMode: VIEW_MODE) => void;
|
||||
}) => {
|
||||
const toggleButtons = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: VIEW_MODE.DOCUMENT_LEVEL,
|
||||
label: i18n.translate('discover.viewModes.document.label', {
|
||||
defaultMessage: 'Documents',
|
||||
}),
|
||||
'data-test-subj': 'dscViewModeDocumentButton',
|
||||
},
|
||||
{
|
||||
id: VIEW_MODE.AGGREGATED_LEVEL,
|
||||
label: (
|
||||
<div className="fieldStatsButton" data-test-subj="dscViewModeFieldStatsButton">
|
||||
<FormattedMessage
|
||||
id="discover.viewModes.fieldStatistics.label"
|
||||
defaultMessage="Field statistics"
|
||||
/>
|
||||
<EuiBetaBadge
|
||||
label={i18n.translate('discover.viewModes.fieldStatistics.betaTitle', {
|
||||
defaultMessage: 'Beta',
|
||||
})}
|
||||
size="s"
|
||||
iconType="beaker"
|
||||
className="fieldStatsBetaBadge"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiButtonGroup
|
||||
className={'dscViewModeToggle'}
|
||||
legend={i18n.translate('discover.viewModes.legend', { defaultMessage: 'View modes' })}
|
||||
buttonSize={'compressed'}
|
||||
options={toggleButtons}
|
||||
idSelected={viewMode}
|
||||
onChange={(id: string) => setDiscoverViewMode(id as VIEW_MODE)}
|
||||
data-test-subj={'dscViewModeToggle'}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -35,6 +35,7 @@ import { DiscoverGridSettings } from '../../../components/discover_grid/types';
|
|||
import { DISCOVER_APP_URL_GENERATOR, DiscoverUrlGeneratorState } from '../../../../url_generator';
|
||||
import { SavedSearch } from '../../../../saved_searches';
|
||||
import { handleSourceColumnState } from '../../../helpers/state_helpers';
|
||||
import { VIEW_MODE } from '../components/view_mode_toggle';
|
||||
|
||||
export interface AppState {
|
||||
/**
|
||||
|
@ -73,6 +74,14 @@ export interface AppState {
|
|||
* id of the used saved query
|
||||
*/
|
||||
savedQuery?: string;
|
||||
/**
|
||||
* Table view: Documents vs Field Statistics
|
||||
*/
|
||||
viewMode?: VIEW_MODE;
|
||||
/**
|
||||
* Hide mini distribution/preview charts when in Field Statistics mode
|
||||
*/
|
||||
hideAggregatedPreview?: boolean;
|
||||
}
|
||||
|
||||
interface GetStateParams {
|
||||
|
|
|
@ -31,6 +31,7 @@ describe('getStateDefaults', () => {
|
|||
"default_column",
|
||||
],
|
||||
"filters": undefined,
|
||||
"hideAggregatedPreview": undefined,
|
||||
"hideChart": undefined,
|
||||
"index": "index-pattern-with-timefield-id",
|
||||
"interval": "auto",
|
||||
|
@ -42,6 +43,7 @@ describe('getStateDefaults', () => {
|
|||
"desc",
|
||||
],
|
||||
],
|
||||
"viewMode": undefined,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
@ -61,12 +63,14 @@ describe('getStateDefaults', () => {
|
|||
"default_column",
|
||||
],
|
||||
"filters": undefined,
|
||||
"hideAggregatedPreview": undefined,
|
||||
"hideChart": undefined,
|
||||
"index": "the-index-pattern-id",
|
||||
"interval": "auto",
|
||||
"query": undefined,
|
||||
"savedQuery": undefined,
|
||||
"sort": Array [],
|
||||
"viewMode": undefined,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -60,6 +60,8 @@ export function getStateDefaults({
|
|||
interval: 'auto',
|
||||
filters: cloneDeep(searchSource.getOwnField('filter')),
|
||||
hideChart: chartHidden ? chartHidden : undefined,
|
||||
viewMode: undefined,
|
||||
hideAggregatedPreview: undefined,
|
||||
savedQuery: undefined,
|
||||
} as AppState;
|
||||
if (savedSearch.grid) {
|
||||
|
@ -68,6 +70,13 @@ export function getStateDefaults({
|
|||
if (savedSearch.hideChart !== undefined) {
|
||||
defaultState.hideChart = savedSearch.hideChart;
|
||||
}
|
||||
if (savedSearch.viewMode) {
|
||||
defaultState.viewMode = savedSearch.viewMode;
|
||||
}
|
||||
|
||||
if (savedSearch.hideAggregatedPreview) {
|
||||
defaultState.hideAggregatedPreview = savedSearch.hideAggregatedPreview;
|
||||
}
|
||||
|
||||
return defaultState;
|
||||
}
|
||||
|
|
|
@ -52,6 +52,14 @@ export async function persistSavedSearch(
|
|||
savedSearch.hideChart = state.hideChart;
|
||||
}
|
||||
|
||||
if (state.viewMode) {
|
||||
savedSearch.viewMode = state.viewMode;
|
||||
}
|
||||
|
||||
if (state.hideAggregatedPreview) {
|
||||
savedSearch.hideAggregatedPreview = state.hideAggregatedPreview;
|
||||
}
|
||||
|
||||
try {
|
||||
const id = await saveSavedSearch(savedSearch, saveOptions, services.core.savedObjects.client);
|
||||
if (id) {
|
||||
|
|
|
@ -0,0 +1,196 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { IndexPatternField, IndexPattern, DataView, Query } from '../../../../../data/common';
|
||||
import { DiscoverServices } from '../../../build_services';
|
||||
import {
|
||||
EmbeddableInput,
|
||||
EmbeddableOutput,
|
||||
ErrorEmbeddable,
|
||||
IEmbeddable,
|
||||
isErrorEmbeddable,
|
||||
} from '../../../../../embeddable/public';
|
||||
import { SavedSearch } from '../../../saved_searches';
|
||||
import { GetStateReturn } from '../../apps/main/services/discover_state';
|
||||
|
||||
export interface DataVisualizerGridEmbeddableInput extends EmbeddableInput {
|
||||
indexPattern: IndexPattern;
|
||||
savedSearch?: SavedSearch;
|
||||
query?: Query;
|
||||
visibleFieldNames?: string[];
|
||||
filters?: Filter[];
|
||||
showPreviewByDefault?: boolean;
|
||||
/**
|
||||
* Callback to add a filter to filter bar
|
||||
*/
|
||||
onAddFilter?: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
|
||||
}
|
||||
export interface DataVisualizerGridEmbeddableOutput extends EmbeddableOutput {
|
||||
showDistributions?: boolean;
|
||||
}
|
||||
|
||||
export interface DiscoverDataVisualizerGridProps {
|
||||
/**
|
||||
* Determines which columns are displayed
|
||||
*/
|
||||
columns: string[];
|
||||
/**
|
||||
* The used index pattern
|
||||
*/
|
||||
indexPattern: DataView;
|
||||
/**
|
||||
* Saved search description
|
||||
*/
|
||||
searchDescription?: string;
|
||||
/**
|
||||
* Saved search title
|
||||
*/
|
||||
searchTitle?: string;
|
||||
/**
|
||||
* Discover plugin services
|
||||
*/
|
||||
services: DiscoverServices;
|
||||
/**
|
||||
* Optional saved search
|
||||
*/
|
||||
savedSearch?: SavedSearch;
|
||||
/**
|
||||
* Optional query to update the table content
|
||||
*/
|
||||
query?: Query;
|
||||
/**
|
||||
* Filters query to update the table content
|
||||
*/
|
||||
filters?: Filter[];
|
||||
stateContainer?: GetStateReturn;
|
||||
/**
|
||||
* Callback to add a filter to filter bar
|
||||
*/
|
||||
onAddFilter?: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
|
||||
}
|
||||
|
||||
export const DiscoverDataVisualizerGrid = (props: DiscoverDataVisualizerGridProps) => {
|
||||
const {
|
||||
services,
|
||||
indexPattern,
|
||||
savedSearch,
|
||||
query,
|
||||
columns,
|
||||
filters,
|
||||
stateContainer,
|
||||
onAddFilter,
|
||||
} = props;
|
||||
const { uiSettings } = services;
|
||||
|
||||
const [embeddable, setEmbeddable] = useState<
|
||||
| ErrorEmbeddable
|
||||
| IEmbeddable<DataVisualizerGridEmbeddableInput, DataVisualizerGridEmbeddableOutput>
|
||||
| undefined
|
||||
>();
|
||||
const embeddableRoot: React.RefObject<HTMLDivElement> = useRef<HTMLDivElement>(null);
|
||||
|
||||
const showPreviewByDefault = useMemo(
|
||||
() =>
|
||||
stateContainer ? !stateContainer.appStateContainer.getState().hideAggregatedPreview : true,
|
||||
[stateContainer]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const sub = embeddable?.getOutput$().subscribe((output: DataVisualizerGridEmbeddableOutput) => {
|
||||
if (output.showDistributions !== undefined && stateContainer) {
|
||||
stateContainer.setAppState({ hideAggregatedPreview: !output.showDistributions });
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
sub?.unsubscribe();
|
||||
};
|
||||
}, [embeddable, stateContainer]);
|
||||
|
||||
useEffect(() => {
|
||||
if (embeddable && !isErrorEmbeddable(embeddable)) {
|
||||
// Update embeddable whenever one of the important input changes
|
||||
embeddable.updateInput({
|
||||
indexPattern,
|
||||
savedSearch,
|
||||
query,
|
||||
filters,
|
||||
visibleFieldNames: columns,
|
||||
onAddFilter,
|
||||
});
|
||||
embeddable.reload();
|
||||
}
|
||||
}, [embeddable, indexPattern, savedSearch, query, columns, filters, onAddFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showPreviewByDefault && embeddable && !isErrorEmbeddable(embeddable)) {
|
||||
// Update embeddable whenever one of the important input changes
|
||||
embeddable.updateInput({
|
||||
showPreviewByDefault,
|
||||
});
|
||||
embeddable.reload();
|
||||
}
|
||||
}, [showPreviewByDefault, uiSettings, embeddable]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// Clean up embeddable upon unmounting
|
||||
embeddable?.destroy();
|
||||
};
|
||||
}, [embeddable]);
|
||||
|
||||
useEffect(() => {
|
||||
let unmounted = false;
|
||||
const loadEmbeddable = async () => {
|
||||
if (services.embeddable) {
|
||||
const factory = services.embeddable.getEmbeddableFactory<
|
||||
DataVisualizerGridEmbeddableInput,
|
||||
DataVisualizerGridEmbeddableOutput
|
||||
>('data_visualizer_grid');
|
||||
if (factory) {
|
||||
// Initialize embeddable with information available at mount
|
||||
const initializedEmbeddable = await factory.create({
|
||||
id: 'discover_data_visualizer_grid',
|
||||
indexPattern,
|
||||
savedSearch,
|
||||
query,
|
||||
showPreviewByDefault,
|
||||
onAddFilter,
|
||||
});
|
||||
if (!unmounted) {
|
||||
setEmbeddable(initializedEmbeddable);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
loadEmbeddable();
|
||||
return () => {
|
||||
unmounted = true;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [services.embeddable, showPreviewByDefault]);
|
||||
|
||||
// We can only render after embeddable has already initialized
|
||||
useEffect(() => {
|
||||
if (embeddableRoot.current && embeddable) {
|
||||
embeddable.render(embeddableRoot.current);
|
||||
}
|
||||
}, [embeddable, embeddableRoot, uiSettings]);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-test-subj="dscFieldStatsEmbeddedContent"
|
||||
ref={embeddableRoot}
|
||||
style={{ height: '100%', overflowY: 'auto', overflowX: 'hidden' }}
|
||||
// Match the scroll bar of the Discover doc table
|
||||
className="kbnDocTableWrapper"
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import {
|
||||
DiscoverDataVisualizerGrid,
|
||||
DiscoverDataVisualizerGridProps,
|
||||
} from './data_visualizer_grid';
|
||||
|
||||
export function FieldStatsTableEmbeddable(renderProps: DiscoverDataVisualizerGridProps) {
|
||||
return (
|
||||
<I18nProvider>
|
||||
<DiscoverDataVisualizerGrid
|
||||
savedSearch={renderProps.savedSearch}
|
||||
services={renderProps.services}
|
||||
indexPattern={renderProps.indexPattern}
|
||||
query={renderProps.query}
|
||||
filters={renderProps.filters}
|
||||
columns={renderProps.columns}
|
||||
stateContainer={renderProps.stateContainer}
|
||||
onAddFilter={renderProps.onAddFilter}
|
||||
/>
|
||||
</I18nProvider>
|
||||
);
|
||||
}
|
|
@ -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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { DiscoverDataVisualizerGrid } from './data_visualizer_grid';
|
|
@ -19,12 +19,12 @@ import { SEARCH_EMBEDDABLE_TYPE } from './constants';
|
|||
import { APPLY_FILTER_TRIGGER, esFilters, FilterManager } from '../../../../data/public';
|
||||
import { DiscoverServices } from '../../build_services';
|
||||
import {
|
||||
Query,
|
||||
TimeRange,
|
||||
Filter,
|
||||
IndexPattern,
|
||||
ISearchSource,
|
||||
IndexPatternField,
|
||||
ISearchSource,
|
||||
Query,
|
||||
TimeRange,
|
||||
} from '../../../../data/common';
|
||||
import { ElasticSearchHit } from '../doc_views/doc_views_types';
|
||||
import { SavedSearchEmbeddableComponent } from './saved_search_embeddable_component';
|
||||
|
@ -35,6 +35,7 @@ import {
|
|||
DOC_TABLE_LEGACY,
|
||||
SAMPLE_SIZE_SETTING,
|
||||
SEARCH_FIELDS_FROM_SOURCE,
|
||||
SHOW_FIELD_STATISTICS,
|
||||
SORT_DEFAULT_ORDER_SETTING,
|
||||
} from '../../../common';
|
||||
import * as columnActions from '../apps/main/components/doc_table/actions/columns';
|
||||
|
@ -45,6 +46,8 @@ import { DocTableProps } from '../apps/main/components/doc_table/doc_table_wrapp
|
|||
import { getDefaultSort } from '../apps/main/components/doc_table';
|
||||
import { SortOrder } from '../apps/main/components/doc_table/components/table_header/helpers';
|
||||
import { updateSearchSource } from './helpers/update_search_source';
|
||||
import { VIEW_MODE } from '../apps/main/components/view_mode_toggle';
|
||||
import { FieldStatsTableEmbeddable } from '../components/data_visualizer_grid/field_stats_table_embeddable';
|
||||
|
||||
export type SearchProps = Partial<DiscoverGridProps> &
|
||||
Partial<DocTableProps> & {
|
||||
|
@ -379,6 +382,28 @@ export class SavedSearchEmbeddable
|
|||
if (!this.searchProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.services.uiSettings.get(SHOW_FIELD_STATISTICS) === true &&
|
||||
this.savedSearch.viewMode === VIEW_MODE.AGGREGATED_LEVEL &&
|
||||
searchProps.services &&
|
||||
searchProps.indexPattern &&
|
||||
Array.isArray(searchProps.columns)
|
||||
) {
|
||||
ReactDOM.render(
|
||||
<FieldStatsTableEmbeddable
|
||||
services={searchProps.services}
|
||||
indexPattern={searchProps.indexPattern}
|
||||
columns={searchProps.columns}
|
||||
savedSearch={this.savedSearch}
|
||||
filters={this.input.filters}
|
||||
query={this.input.query}
|
||||
onAddFilter={searchProps.onFilter}
|
||||
/>,
|
||||
domNode
|
||||
);
|
||||
return;
|
||||
}
|
||||
const useLegacyTable = this.services.uiSettings.get(DOC_TABLE_LEGACY);
|
||||
const props = {
|
||||
searchProps,
|
||||
|
|
|
@ -37,6 +37,7 @@ import { UrlForwardingStart } from '../../url_forwarding/public';
|
|||
import { NavigationPublicPluginStart } from '../../navigation/public';
|
||||
import { IndexPatternFieldEditorStart } from '../../index_pattern_field_editor/public';
|
||||
import { FieldFormatsStart } from '../../field_formats/public';
|
||||
import { EmbeddableStart } from '../../embeddable/public';
|
||||
|
||||
import type { SpacesApi } from '../../../../x-pack/plugins/spaces/public';
|
||||
|
||||
|
@ -47,6 +48,7 @@ export interface DiscoverServices {
|
|||
core: CoreStart;
|
||||
data: DataPublicPluginStart;
|
||||
docLinks: DocLinksStart;
|
||||
embeddable: EmbeddableStart;
|
||||
history: () => History;
|
||||
theme: ChartsPluginStart['theme'];
|
||||
filterManager: FilterManager;
|
||||
|
@ -83,6 +85,7 @@ export function buildServices(
|
|||
core,
|
||||
data: plugins.data,
|
||||
docLinks: core.docLinks,
|
||||
embeddable: plugins.embeddable,
|
||||
theme: plugins.charts.theme,
|
||||
fieldFormats: plugins.fieldFormats,
|
||||
filterManager: plugins.data.query.filterManager,
|
||||
|
|
|
@ -348,6 +348,11 @@ export class DiscoverPlugin
|
|||
await depsStart.data.indexPatterns.clearCache();
|
||||
|
||||
const { renderApp } = await import('./application');
|
||||
|
||||
// FIXME: Temporarily hide overflow-y in Discover app when Field Stats table is shown
|
||||
// due to EUI bug https://github.com/elastic/eui/pull/5152
|
||||
params.element.classList.add('dscAppWrapper');
|
||||
|
||||
const unmount = renderApp(params.element);
|
||||
return () => {
|
||||
unlistenParentHistory();
|
||||
|
|
|
@ -101,6 +101,7 @@ describe('getSavedSearch', () => {
|
|||
],
|
||||
"description": "description",
|
||||
"grid": Object {},
|
||||
"hideAggregatedPreview": undefined,
|
||||
"hideChart": false,
|
||||
"id": "ccf1af80-2297-11ec-86e0-1155ffb9c7a7",
|
||||
"searchSource": Object {
|
||||
|
@ -138,6 +139,7 @@ describe('getSavedSearch', () => {
|
|||
],
|
||||
],
|
||||
"title": "test1",
|
||||
"viewMode": undefined,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -54,6 +54,7 @@ describe('saved_searches_utils', () => {
|
|||
],
|
||||
"description": "foo",
|
||||
"grid": Object {},
|
||||
"hideAggregatedPreview": undefined,
|
||||
"hideChart": true,
|
||||
"id": "id",
|
||||
"searchSource": SearchSource {
|
||||
|
@ -74,6 +75,7 @@ describe('saved_searches_utils', () => {
|
|||
"sharingSavedObjectProps": Object {},
|
||||
"sort": Array [],
|
||||
"title": "saved search",
|
||||
"viewMode": undefined,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
@ -122,6 +124,7 @@ describe('saved_searches_utils', () => {
|
|||
],
|
||||
"description": "description",
|
||||
"grid": Object {},
|
||||
"hideAggregatedPreview": undefined,
|
||||
"hideChart": true,
|
||||
"kibanaSavedObjectMeta": Object {
|
||||
"searchSourceJSON": "{}",
|
||||
|
@ -133,6 +136,7 @@ describe('saved_searches_utils', () => {
|
|||
],
|
||||
],
|
||||
"title": "title",
|
||||
"viewMode": undefined,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -41,6 +41,8 @@ export const fromSavedSearchAttributes = (
|
|||
description: attributes.description,
|
||||
grid: attributes.grid,
|
||||
hideChart: attributes.hideChart,
|
||||
viewMode: attributes.viewMode,
|
||||
hideAggregatedPreview: attributes.hideAggregatedPreview,
|
||||
});
|
||||
|
||||
export const toSavedSearchAttributes = (
|
||||
|
@ -54,4 +56,6 @@ export const toSavedSearchAttributes = (
|
|||
description: savedSearch.description ?? '',
|
||||
grid: savedSearch.grid ?? {},
|
||||
hideChart: savedSearch.hideChart ?? false,
|
||||
viewMode: savedSearch.viewMode,
|
||||
hideAggregatedPreview: savedSearch.hideAggregatedPreview,
|
||||
});
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import type { ISearchSource } from '../../../data/public';
|
||||
import { DiscoverGridSettingsColumn } from '../application/components/discover_grid/types';
|
||||
import { VIEW_MODE } from '../application/apps/main/components/view_mode_toggle';
|
||||
|
||||
/** @internal **/
|
||||
export interface SavedSearchAttributes {
|
||||
|
@ -22,6 +23,8 @@ export interface SavedSearchAttributes {
|
|||
kibanaSavedObjectMeta: {
|
||||
searchSourceJSON: string;
|
||||
};
|
||||
viewMode?: VIEW_MODE;
|
||||
hideAggregatedPreview?: boolean;
|
||||
}
|
||||
|
||||
/** @internal **/
|
||||
|
@ -44,4 +47,6 @@ export interface SavedSearch {
|
|||
aliasTargetId?: string;
|
||||
errorJSON?: string;
|
||||
};
|
||||
viewMode?: VIEW_MODE;
|
||||
hideAggregatedPreview?: boolean;
|
||||
}
|
||||
|
|
|
@ -32,7 +32,9 @@ export const searchSavedObjectType: SavedObjectsType = {
|
|||
properties: {
|
||||
columns: { type: 'keyword', index: false, doc_values: false },
|
||||
description: { type: 'text' },
|
||||
viewMode: { type: 'keyword', index: false, doc_values: false },
|
||||
hideChart: { type: 'boolean', index: false, doc_values: false },
|
||||
hideAggregatedPreview: { type: 'boolean', index: false, doc_values: false },
|
||||
hits: { type: 'integer', index: false, doc_values: false },
|
||||
kibanaSavedObjectMeta: {
|
||||
properties: {
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
SEARCH_FIELDS_FROM_SOURCE,
|
||||
MAX_DOC_FIELDS_DISPLAYED,
|
||||
SHOW_MULTIFIELDS,
|
||||
SHOW_FIELD_STATISTICS,
|
||||
} from '../common';
|
||||
|
||||
export const getUiSettings: () => Record<string, UiSettingsParams> = () => ({
|
||||
|
@ -172,6 +173,7 @@ export const getUiSettings: () => Record<string, UiSettingsParams> = () => ({
|
|||
name: 'discover:useLegacyDataGrid',
|
||||
},
|
||||
},
|
||||
|
||||
[MODIFY_COLUMNS_ON_SWITCH]: {
|
||||
name: i18n.translate('discover.advancedSettings.discover.modifyColumnsOnSwitchTitle', {
|
||||
defaultMessage: 'Modify columns when changing data views',
|
||||
|
@ -201,6 +203,24 @@ export const getUiSettings: () => Record<string, UiSettingsParams> = () => ({
|
|||
category: ['discover'],
|
||||
schema: schema.boolean(),
|
||||
},
|
||||
[SHOW_FIELD_STATISTICS]: {
|
||||
name: i18n.translate('discover.advancedSettings.discover.showFieldStatistics', {
|
||||
defaultMessage: 'Show field statistics',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'discover.advancedSettings.discover.showFieldStatisticsDescription',
|
||||
{
|
||||
defaultMessage: `Enable "Field statistics" table in Discover.`,
|
||||
}
|
||||
),
|
||||
value: false,
|
||||
category: ['discover'],
|
||||
schema: schema.boolean(),
|
||||
metric: {
|
||||
type: METRIC_TYPE.CLICK,
|
||||
name: 'discover:showFieldStatistics',
|
||||
},
|
||||
},
|
||||
[SHOW_MULTIFIELDS]: {
|
||||
name: i18n.translate('discover.advancedSettings.discover.showMultifields', {
|
||||
defaultMessage: 'Show multi-fields',
|
||||
|
|
|
@ -448,6 +448,10 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = {
|
|||
type: 'boolean',
|
||||
_meta: { description: 'Non-default value of setting.' },
|
||||
},
|
||||
'discover:showFieldStatistics': {
|
||||
type: 'boolean',
|
||||
_meta: { description: 'Non-default value of setting.' },
|
||||
},
|
||||
'discover:showMultiFields': {
|
||||
type: 'boolean',
|
||||
_meta: { description: 'Non-default value of setting.' },
|
||||
|
|
|
@ -31,6 +31,7 @@ export interface UsageStats {
|
|||
'doc_table:legacy': boolean;
|
||||
'discover:modifyColumnsOnSwitch': boolean;
|
||||
'discover:searchFieldsFromSource': boolean;
|
||||
'discover:showFieldStatistics': boolean;
|
||||
'discover:showMultiFields': boolean;
|
||||
'discover:maxDocFieldsDisplayed': number;
|
||||
'securitySolution:rulesTableRefresh': string;
|
||||
|
|
|
@ -7689,6 +7689,12 @@
|
|||
"description": "Non-default value of setting."
|
||||
}
|
||||
},
|
||||
"discover:showFieldStatistics": {
|
||||
"type": "boolean",
|
||||
"_meta": {
|
||||
"description": "Non-default value of setting."
|
||||
}
|
||||
},
|
||||
"discover:showMultiFields": {
|
||||
"type": "boolean",
|
||||
"_meta": {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrService } from '../ftr_provider_context';
|
||||
|
||||
export class DiscoverPageObject extends FtrService {
|
||||
|
@ -307,6 +308,13 @@ export class DiscoverPageObject extends FtrService {
|
|||
return await this.testSubjects.click('collapseSideBarButton');
|
||||
}
|
||||
|
||||
public async closeSidebar() {
|
||||
await this.retry.tryForTime(2 * 1000, async () => {
|
||||
await this.toggleSidebarCollapse();
|
||||
await this.testSubjects.missingOrFail('discover-sidebar');
|
||||
});
|
||||
}
|
||||
|
||||
public async getAllFieldNames() {
|
||||
const sidebar = await this.testSubjects.find('discover-sidebar');
|
||||
const $ = await sidebar.parseDomContent();
|
||||
|
@ -545,4 +553,37 @@ export class DiscoverPageObject extends FtrService {
|
|||
public async clearSavedQuery() {
|
||||
await this.testSubjects.click('saved-query-management-clear-button');
|
||||
}
|
||||
|
||||
public async assertHitCount(expectedHitCount: string) {
|
||||
await this.retry.tryForTime(2 * 1000, async () => {
|
||||
// Close side bar to ensure Discover hit count shows
|
||||
// edge case for when browser width is small
|
||||
await this.closeSidebar();
|
||||
const hitCount = await this.getHitCount();
|
||||
expect(hitCount).to.eql(
|
||||
expectedHitCount,
|
||||
`Expected Discover hit count to be ${expectedHitCount} but got ${hitCount}.`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public async assertViewModeToggleNotExists() {
|
||||
await this.testSubjects.missingOrFail('dscViewModeToggle', { timeout: 2 * 1000 });
|
||||
}
|
||||
|
||||
public async assertViewModeToggleExists() {
|
||||
await this.testSubjects.existOrFail('dscViewModeToggle', { timeout: 2 * 1000 });
|
||||
}
|
||||
|
||||
public async assertFieldStatsTableNotExists() {
|
||||
await this.testSubjects.missingOrFail('dscFieldStatsEmbeddedContent', { timeout: 2 * 1000 });
|
||||
}
|
||||
|
||||
public async clickViewModeFieldStatsButton() {
|
||||
await this.retry.tryForTime(2 * 1000, async () => {
|
||||
await this.testSubjects.existOrFail('dscViewModeFieldStatsButton');
|
||||
await this.testSubjects.clickWhenNotDisabled('dscViewModeFieldStatsButton');
|
||||
await this.testSubjects.existOrFail('dscFieldStatsEmbeddedContent');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ exports[`FieldTypeIcon render component when type matches a field type 1`] = `
|
|||
>
|
||||
<FieldTypeIconContainer
|
||||
ariaLabel="keyword type"
|
||||
iconType="tokenKeyword"
|
||||
iconType="tokenString"
|
||||
needsAria={false}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
|
|
|
@ -48,7 +48,7 @@ export const typeToEuiIconMap: Record<string, { iconType: string; color?: string
|
|||
_source: { iconType: 'editorCodeBlock', color: 'gray' },
|
||||
string: { iconType: 'tokenString' },
|
||||
text: { iconType: 'tokenString' },
|
||||
keyword: { iconType: 'tokenKeyword' },
|
||||
keyword: { iconType: 'tokenString' },
|
||||
nested: { iconType: 'tokenNested' },
|
||||
};
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import { parse } from 'query-string';
|
||||
import { createContext, useCallback, useContext, useMemo } from 'react';
|
||||
// @ts-ignore
|
||||
import { decode } from 'rison-node';
|
||||
|
||||
export interface Dictionary<TValue> {
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const DATA_VISUALIZER_GRID_EMBEDDABLE_TYPE = 'data_visualizer_grid';
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { EuiLoadingSpinner, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
|
||||
export const EmbeddableLoading = () => {
|
||||
return (
|
||||
<EuiText textAlign="center">
|
||||
<EuiSpacer size="l" />
|
||||
<EuiLoadingSpinner size="l" />
|
||||
<EuiSpacer size="l" />
|
||||
</EuiText>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,234 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { CoreStart } from 'kibana/public';
|
||||
import ReactDOM from 'react-dom';
|
||||
import React, { Suspense, useCallback, useState } from 'react';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { EuiEmptyPrompt, EuiIcon, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { Required } from 'utility-types';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
Embeddable,
|
||||
EmbeddableInput,
|
||||
EmbeddableOutput,
|
||||
IContainer,
|
||||
} from '../../../../../../../../src/plugins/embeddable/public';
|
||||
import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { DATA_VISUALIZER_GRID_EMBEDDABLE_TYPE } from './constants';
|
||||
import { EmbeddableLoading } from './embeddable_loading_fallback';
|
||||
import { DataVisualizerStartDependencies } from '../../../../plugin';
|
||||
import {
|
||||
IndexPattern,
|
||||
IndexPatternField,
|
||||
Query,
|
||||
} from '../../../../../../../../src/plugins/data/common';
|
||||
import { SavedSearch } from '../../../../../../../../src/plugins/discover/public';
|
||||
import {
|
||||
DataVisualizerTable,
|
||||
ItemIdToExpandedRowMap,
|
||||
} from '../../../common/components/stats_table';
|
||||
import { FieldVisConfig } from '../../../common/components/stats_table/types';
|
||||
import { getDefaultDataVisualizerListState } from '../../components/index_data_visualizer_view/index_data_visualizer_view';
|
||||
import { DataVisualizerTableState } from '../../../../../common';
|
||||
import { DataVisualizerIndexBasedAppState } from '../../types/index_data_visualizer_state';
|
||||
import { IndexBasedDataVisualizerExpandedRow } from '../../../common/components/expanded_row/index_based_expanded_row';
|
||||
import { useDataVisualizerGridData } from './use_data_visualizer_grid_data';
|
||||
|
||||
export type DataVisualizerGridEmbeddableServices = [CoreStart, DataVisualizerStartDependencies];
|
||||
export interface DataVisualizerGridEmbeddableInput extends EmbeddableInput {
|
||||
indexPattern: IndexPattern;
|
||||
savedSearch?: SavedSearch;
|
||||
query?: Query;
|
||||
visibleFieldNames?: string[];
|
||||
filters?: Filter[];
|
||||
showPreviewByDefault?: boolean;
|
||||
/**
|
||||
* Callback to add a filter to filter bar
|
||||
*/
|
||||
onAddFilter?: (field: IndexPatternField | string, value: string, type: '+' | '-') => void;
|
||||
}
|
||||
export type DataVisualizerGridEmbeddableOutput = EmbeddableOutput;
|
||||
|
||||
export type IDataVisualizerGridEmbeddable = typeof DataVisualizerGridEmbeddable;
|
||||
|
||||
const restorableDefaults = getDefaultDataVisualizerListState();
|
||||
|
||||
export const EmbeddableWrapper = ({
|
||||
input,
|
||||
onOutputChange,
|
||||
}: {
|
||||
input: DataVisualizerGridEmbeddableInput;
|
||||
onOutputChange?: (ouput: any) => void;
|
||||
}) => {
|
||||
const [dataVisualizerListState, setDataVisualizerListState] =
|
||||
useState<Required<DataVisualizerIndexBasedAppState>>(restorableDefaults);
|
||||
|
||||
const onTableChange = useCallback(
|
||||
(update: DataVisualizerTableState) => {
|
||||
setDataVisualizerListState({ ...dataVisualizerListState, ...update });
|
||||
if (onOutputChange) {
|
||||
onOutputChange(update);
|
||||
}
|
||||
},
|
||||
[dataVisualizerListState, onOutputChange]
|
||||
);
|
||||
const { configs, searchQueryLanguage, searchString, extendedColumns, loaded } =
|
||||
useDataVisualizerGridData(input, dataVisualizerListState);
|
||||
const getItemIdToExpandedRowMap = useCallback(
|
||||
function (itemIds: string[], items: FieldVisConfig[]): ItemIdToExpandedRowMap {
|
||||
return itemIds.reduce((m: ItemIdToExpandedRowMap, fieldName: string) => {
|
||||
const item = items.find((fieldVisConfig) => fieldVisConfig.fieldName === fieldName);
|
||||
if (item !== undefined) {
|
||||
m[fieldName] = (
|
||||
<IndexBasedDataVisualizerExpandedRow
|
||||
item={item}
|
||||
indexPattern={input.indexPattern}
|
||||
combinedQuery={{ searchQueryLanguage, searchString }}
|
||||
onAddFilter={input.onAddFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return m;
|
||||
}, {} as ItemIdToExpandedRowMap);
|
||||
},
|
||||
[input, searchQueryLanguage, searchString]
|
||||
);
|
||||
|
||||
if (
|
||||
loaded &&
|
||||
(configs.length === 0 ||
|
||||
// FIXME: Configs might have a placeholder document count stats field
|
||||
// This will be removed in the future
|
||||
(configs.length === 1 && configs[0].fieldName === undefined))
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
flex: '1 0 100%',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<EuiText size="xs" color="subdued">
|
||||
<EuiIcon type="visualizeApp" size="m" color="subdued" />
|
||||
<EuiSpacer size="m" />
|
||||
<FormattedMessage
|
||||
id="xpack.dataVisualizer.index.embeddableNoResultsMessage"
|
||||
defaultMessage="No results found"
|
||||
/>
|
||||
</EuiText>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<DataVisualizerTable<FieldVisConfig>
|
||||
items={configs}
|
||||
pageState={dataVisualizerListState}
|
||||
updatePageState={onTableChange}
|
||||
getItemIdToExpandedRowMap={getItemIdToExpandedRowMap}
|
||||
extendedColumns={extendedColumns}
|
||||
showPreviewByDefault={input?.showPreviewByDefault}
|
||||
onChange={onOutputChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const IndexDataVisualizerViewWrapper = (props: {
|
||||
id: string;
|
||||
embeddableContext: InstanceType<IDataVisualizerGridEmbeddable>;
|
||||
embeddableInput: Readonly<Observable<DataVisualizerGridEmbeddableInput>>;
|
||||
onOutputChange?: (output: any) => void;
|
||||
}) => {
|
||||
const { embeddableInput, onOutputChange } = props;
|
||||
|
||||
const input = useObservable(embeddableInput);
|
||||
if (input && input.indexPattern) {
|
||||
return <EmbeddableWrapper input={input} onOutputChange={onOutputChange} />;
|
||||
} else {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
iconType="alert"
|
||||
iconColor="danger"
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="xpack.dataVisualizer.index.embeddableErrorTitle"
|
||||
defaultMessage="Error loading embeddable"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.dataVisualizer.index.embeddableErrorDescription"
|
||||
defaultMessage="There was an error loading the embeddable. Please check if all the required input is valid."
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
export class DataVisualizerGridEmbeddable extends Embeddable<
|
||||
DataVisualizerGridEmbeddableInput,
|
||||
DataVisualizerGridEmbeddableOutput
|
||||
> {
|
||||
private node?: HTMLElement;
|
||||
private reload$ = new Subject();
|
||||
public readonly type: string = DATA_VISUALIZER_GRID_EMBEDDABLE_TYPE;
|
||||
|
||||
constructor(
|
||||
initialInput: DataVisualizerGridEmbeddableInput,
|
||||
public services: DataVisualizerGridEmbeddableServices,
|
||||
parent?: IContainer
|
||||
) {
|
||||
super(initialInput, {}, parent);
|
||||
}
|
||||
|
||||
public render(node: HTMLElement) {
|
||||
super.render(node);
|
||||
this.node = node;
|
||||
|
||||
const I18nContext = this.services[0].i18n.Context;
|
||||
|
||||
ReactDOM.render(
|
||||
<I18nContext>
|
||||
<KibanaContextProvider services={{ ...this.services[0], ...this.services[1] }}>
|
||||
<Suspense fallback={<EmbeddableLoading />}>
|
||||
<IndexDataVisualizerViewWrapper
|
||||
id={this.input.id}
|
||||
embeddableContext={this}
|
||||
embeddableInput={this.getInput$()}
|
||||
onOutputChange={(output) => this.updateOutput(output)}
|
||||
/>
|
||||
</Suspense>
|
||||
</KibanaContextProvider>
|
||||
</I18nContext>,
|
||||
node
|
||||
);
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
super.destroy();
|
||||
if (this.node) {
|
||||
ReactDOM.unmountComponentAtNode(this.node);
|
||||
}
|
||||
}
|
||||
|
||||
public reload() {
|
||||
this.reload$.next();
|
||||
}
|
||||
|
||||
public supportedTriggers() {
|
||||
return [];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { StartServicesAccessor } from 'kibana/public';
|
||||
import {
|
||||
EmbeddableFactoryDefinition,
|
||||
IContainer,
|
||||
} from '../../../../../../../../src/plugins/embeddable/public';
|
||||
import { DATA_VISUALIZER_GRID_EMBEDDABLE_TYPE } from './constants';
|
||||
import {
|
||||
DataVisualizerGridEmbeddableInput,
|
||||
DataVisualizerGridEmbeddableServices,
|
||||
} from './grid_embeddable';
|
||||
import { DataVisualizerPluginStart, DataVisualizerStartDependencies } from '../../../../plugin';
|
||||
|
||||
export class DataVisualizerGridEmbeddableFactory
|
||||
implements EmbeddableFactoryDefinition<DataVisualizerGridEmbeddableInput>
|
||||
{
|
||||
public readonly type = DATA_VISUALIZER_GRID_EMBEDDABLE_TYPE;
|
||||
|
||||
public readonly grouping = [
|
||||
{
|
||||
id: 'data_visualizer_grid',
|
||||
getDisplayName: () => 'Data Visualizer Grid',
|
||||
},
|
||||
];
|
||||
|
||||
constructor(
|
||||
private getStartServices: StartServicesAccessor<
|
||||
DataVisualizerStartDependencies,
|
||||
DataVisualizerPluginStart
|
||||
>
|
||||
) {}
|
||||
|
||||
public async isEditable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public canCreateNew() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public getDisplayName() {
|
||||
return i18n.translate('xpack.dataVisualizer.index.components.grid.displayName', {
|
||||
defaultMessage: 'Data visualizer grid',
|
||||
});
|
||||
}
|
||||
|
||||
public getDescription() {
|
||||
return i18n.translate('xpack.dataVisualizer.index.components.grid.description', {
|
||||
defaultMessage: 'Visualize data',
|
||||
});
|
||||
}
|
||||
|
||||
private async getServices(): Promise<DataVisualizerGridEmbeddableServices> {
|
||||
const [coreStart, pluginsStart] = await this.getStartServices();
|
||||
return [coreStart, pluginsStart];
|
||||
}
|
||||
|
||||
public async create(initialInput: DataVisualizerGridEmbeddableInput, parent?: IContainer) {
|
||||
const [coreStart, pluginsStart] = await this.getServices();
|
||||
const { DataVisualizerGridEmbeddable } = await import('./grid_embeddable');
|
||||
return new DataVisualizerGridEmbeddable(initialInput, [coreStart, pluginsStart], parent);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
export { DataVisualizerGridEmbeddable } from './grid_embeddable';
|
||||
export { DataVisualizerGridEmbeddableFactory } from './grid_embeddable_factory';
|
|
@ -0,0 +1,587 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Required } from 'utility-types';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { merge } from 'rxjs';
|
||||
import { EuiTableActionsColumnType } from '@elastic/eui/src/components/basic_table/table_types';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { DataVisualizerIndexBasedAppState } from '../../types/index_data_visualizer_state';
|
||||
import { useDataVisualizerKibana } from '../../../kibana_context';
|
||||
import { getEsQueryFromSavedSearch } from '../../utils/saved_search_utils';
|
||||
import { MetricFieldsStats } from '../../../common/components/stats_table/components/field_count_stats';
|
||||
import { DataLoader } from '../../data_loader/data_loader';
|
||||
import { useTimefilter } from '../../hooks/use_time_filter';
|
||||
import { dataVisualizerRefresh$ } from '../../services/timefilter_refresh_service';
|
||||
import { TimeBuckets } from '../../services/time_buckets';
|
||||
import {
|
||||
DataViewField,
|
||||
KBN_FIELD_TYPES,
|
||||
UI_SETTINGS,
|
||||
} from '../../../../../../../../src/plugins/data/common';
|
||||
import { extractErrorProperties } from '../../utils/error_utils';
|
||||
import { FieldVisConfig } from '../../../common/components/stats_table/types';
|
||||
import { FieldRequestConfig, JOB_FIELD_TYPES } from '../../../../../common';
|
||||
import { kbnTypeToJobType } from '../../../common/util/field_types_utils';
|
||||
import { getActions } from '../../../common/components/field_data_row/action_menu';
|
||||
import { DataVisualizerGridEmbeddableInput } from './grid_embeddable';
|
||||
import { getDefaultPageState } from '../../components/index_data_visualizer_view/index_data_visualizer_view';
|
||||
|
||||
const defaults = getDefaultPageState();
|
||||
|
||||
export const useDataVisualizerGridData = (
|
||||
input: DataVisualizerGridEmbeddableInput,
|
||||
dataVisualizerListState: Required<DataVisualizerIndexBasedAppState>
|
||||
) => {
|
||||
const { services } = useDataVisualizerKibana();
|
||||
const { notifications, uiSettings } = services;
|
||||
const { toasts } = notifications;
|
||||
const { samplerShardSize, visibleFieldTypes, showEmptyFields } = dataVisualizerListState;
|
||||
|
||||
const [lastRefresh, setLastRefresh] = useState(0);
|
||||
|
||||
const {
|
||||
currentSavedSearch,
|
||||
currentIndexPattern,
|
||||
currentQuery,
|
||||
currentFilters,
|
||||
visibleFieldNames,
|
||||
} = useMemo(
|
||||
() => ({
|
||||
currentSavedSearch: input?.savedSearch,
|
||||
currentIndexPattern: input.indexPattern,
|
||||
currentQuery: input?.query,
|
||||
visibleFieldNames: input?.visibleFieldNames ?? [],
|
||||
currentFilters: input?.filters,
|
||||
}),
|
||||
[input]
|
||||
);
|
||||
|
||||
const { searchQueryLanguage, searchString, searchQuery } = useMemo(() => {
|
||||
const searchData = getEsQueryFromSavedSearch({
|
||||
indexPattern: currentIndexPattern,
|
||||
uiSettings,
|
||||
savedSearch: currentSavedSearch,
|
||||
query: currentQuery,
|
||||
filters: currentFilters,
|
||||
});
|
||||
|
||||
if (searchData === undefined || dataVisualizerListState.searchString !== '') {
|
||||
return {
|
||||
searchQuery: dataVisualizerListState.searchQuery,
|
||||
searchString: dataVisualizerListState.searchString,
|
||||
searchQueryLanguage: dataVisualizerListState.searchQueryLanguage,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
searchQuery: searchData.searchQuery,
|
||||
searchString: searchData.searchString,
|
||||
searchQueryLanguage: searchData.queryLanguage,
|
||||
};
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
currentSavedSearch,
|
||||
currentIndexPattern,
|
||||
dataVisualizerListState,
|
||||
currentQuery,
|
||||
currentFilters,
|
||||
]);
|
||||
|
||||
const [overallStats, setOverallStats] = useState(defaults.overallStats);
|
||||
|
||||
const [documentCountStats, setDocumentCountStats] = useState(defaults.documentCountStats);
|
||||
const [metricConfigs, setMetricConfigs] = useState(defaults.metricConfigs);
|
||||
const [metricsLoaded, setMetricsLoaded] = useState(defaults.metricsLoaded);
|
||||
const [metricsStats, setMetricsStats] = useState<undefined | MetricFieldsStats>();
|
||||
|
||||
const [nonMetricConfigs, setNonMetricConfigs] = useState(defaults.nonMetricConfigs);
|
||||
const [nonMetricsLoaded, setNonMetricsLoaded] = useState(defaults.nonMetricsLoaded);
|
||||
|
||||
const dataLoader = useMemo(
|
||||
() => new DataLoader(currentIndexPattern, toasts),
|
||||
[currentIndexPattern, toasts]
|
||||
);
|
||||
|
||||
const timefilter = useTimefilter({
|
||||
timeRangeSelector: currentIndexPattern?.timeFieldName !== undefined,
|
||||
autoRefreshSelector: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const timeUpdateSubscription = merge(
|
||||
timefilter.getTimeUpdate$(),
|
||||
dataVisualizerRefresh$
|
||||
).subscribe(() => {
|
||||
setLastRefresh(Date.now());
|
||||
});
|
||||
return () => {
|
||||
timeUpdateSubscription.unsubscribe();
|
||||
};
|
||||
});
|
||||
|
||||
const getTimeBuckets = useCallback(() => {
|
||||
return new TimeBuckets({
|
||||
[UI_SETTINGS.HISTOGRAM_MAX_BARS]: uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS),
|
||||
[UI_SETTINGS.HISTOGRAM_BAR_TARGET]: uiSettings.get(UI_SETTINGS.HISTOGRAM_BAR_TARGET),
|
||||
dateFormat: uiSettings.get('dateFormat'),
|
||||
'dateFormat:scaled': uiSettings.get('dateFormat:scaled'),
|
||||
});
|
||||
}, [uiSettings]);
|
||||
|
||||
const indexPatternFields: DataViewField[] = useMemo(
|
||||
() => currentIndexPattern.fields,
|
||||
[currentIndexPattern]
|
||||
);
|
||||
|
||||
async function loadOverallStats() {
|
||||
const tf = timefilter as any;
|
||||
let earliest;
|
||||
let latest;
|
||||
|
||||
const activeBounds = tf.getActiveBounds();
|
||||
|
||||
if (currentIndexPattern.timeFieldName !== undefined && activeBounds === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentIndexPattern.timeFieldName !== undefined) {
|
||||
earliest = activeBounds.min.valueOf();
|
||||
latest = activeBounds.max.valueOf();
|
||||
}
|
||||
|
||||
try {
|
||||
const allStats = await dataLoader.loadOverallData(
|
||||
searchQuery,
|
||||
samplerShardSize,
|
||||
earliest,
|
||||
latest
|
||||
);
|
||||
// Because load overall stats perform queries in batches
|
||||
// there could be multiple errors
|
||||
if (Array.isArray(allStats.errors) && allStats.errors.length > 0) {
|
||||
allStats.errors.forEach((err: any) => {
|
||||
dataLoader.displayError(extractErrorProperties(err));
|
||||
});
|
||||
}
|
||||
setOverallStats(allStats);
|
||||
} catch (err) {
|
||||
dataLoader.displayError(err.body ?? err);
|
||||
}
|
||||
}
|
||||
|
||||
const createMetricCards = useCallback(() => {
|
||||
const configs: FieldVisConfig[] = [];
|
||||
const aggregatableExistsFields: any[] = overallStats.aggregatableExistsFields || [];
|
||||
|
||||
const allMetricFields = indexPatternFields.filter((f) => {
|
||||
return (
|
||||
f.type === KBN_FIELD_TYPES.NUMBER &&
|
||||
f.displayName !== undefined &&
|
||||
dataLoader.isDisplayField(f.displayName) === true
|
||||
);
|
||||
});
|
||||
const metricExistsFields = allMetricFields.filter((f) => {
|
||||
return aggregatableExistsFields.find((existsF) => {
|
||||
return existsF.fieldName === f.spec.name;
|
||||
});
|
||||
});
|
||||
|
||||
// Add a config for 'document count', identified by no field name if indexpattern is time based.
|
||||
if (currentIndexPattern.timeFieldName !== undefined) {
|
||||
configs.push({
|
||||
type: JOB_FIELD_TYPES.NUMBER,
|
||||
existsInDocs: true,
|
||||
loading: true,
|
||||
aggregatable: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (metricsLoaded === false) {
|
||||
setMetricsLoaded(true);
|
||||
return;
|
||||
}
|
||||
|
||||
let aggregatableFields: any[] = overallStats.aggregatableExistsFields;
|
||||
if (allMetricFields.length !== metricExistsFields.length && metricsLoaded === true) {
|
||||
aggregatableFields = aggregatableFields.concat(overallStats.aggregatableNotExistsFields);
|
||||
}
|
||||
|
||||
const metricFieldsToShow =
|
||||
metricsLoaded === true && showEmptyFields === true ? allMetricFields : metricExistsFields;
|
||||
|
||||
metricFieldsToShow.forEach((field) => {
|
||||
const fieldData = aggregatableFields.find((f) => {
|
||||
return f.fieldName === field.spec.name;
|
||||
});
|
||||
|
||||
const metricConfig: FieldVisConfig = {
|
||||
...(fieldData ? fieldData : {}),
|
||||
fieldFormat: currentIndexPattern.getFormatterForField(field),
|
||||
type: JOB_FIELD_TYPES.NUMBER,
|
||||
loading: true,
|
||||
aggregatable: true,
|
||||
deletable: field.runtimeField !== undefined,
|
||||
};
|
||||
if (field.displayName !== metricConfig.fieldName) {
|
||||
metricConfig.displayName = field.displayName;
|
||||
}
|
||||
|
||||
configs.push(metricConfig);
|
||||
});
|
||||
|
||||
setMetricsStats({
|
||||
totalMetricFieldsCount: allMetricFields.length,
|
||||
visibleMetricsCount: metricFieldsToShow.length,
|
||||
});
|
||||
setMetricConfigs(configs);
|
||||
}, [
|
||||
currentIndexPattern,
|
||||
dataLoader,
|
||||
indexPatternFields,
|
||||
metricsLoaded,
|
||||
overallStats,
|
||||
showEmptyFields,
|
||||
]);
|
||||
|
||||
const createNonMetricCards = useCallback(() => {
|
||||
const allNonMetricFields = indexPatternFields.filter((f) => {
|
||||
return (
|
||||
f.type !== KBN_FIELD_TYPES.NUMBER &&
|
||||
f.displayName !== undefined &&
|
||||
dataLoader.isDisplayField(f.displayName) === true
|
||||
);
|
||||
});
|
||||
// Obtain the list of all non-metric fields which appear in documents
|
||||
// (aggregatable or not aggregatable).
|
||||
const populatedNonMetricFields: any[] = []; // Kibana index pattern non metric fields.
|
||||
let nonMetricFieldData: any[] = []; // Basic non metric field data loaded from requesting overall stats.
|
||||
const aggregatableExistsFields: any[] = overallStats.aggregatableExistsFields || [];
|
||||
const nonAggregatableExistsFields: any[] = overallStats.nonAggregatableExistsFields || [];
|
||||
|
||||
allNonMetricFields.forEach((f) => {
|
||||
const checkAggregatableField = aggregatableExistsFields.find(
|
||||
(existsField) => existsField.fieldName === f.spec.name
|
||||
);
|
||||
|
||||
if (checkAggregatableField !== undefined) {
|
||||
populatedNonMetricFields.push(f);
|
||||
nonMetricFieldData.push(checkAggregatableField);
|
||||
} else {
|
||||
const checkNonAggregatableField = nonAggregatableExistsFields.find(
|
||||
(existsField) => existsField.fieldName === f.spec.name
|
||||
);
|
||||
|
||||
if (checkNonAggregatableField !== undefined) {
|
||||
populatedNonMetricFields.push(f);
|
||||
nonMetricFieldData.push(checkNonAggregatableField);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (nonMetricsLoaded === false) {
|
||||
setNonMetricsLoaded(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (allNonMetricFields.length !== nonMetricFieldData.length && showEmptyFields === true) {
|
||||
// Combine the field data obtained from Elasticsearch into a single array.
|
||||
nonMetricFieldData = nonMetricFieldData.concat(
|
||||
overallStats.aggregatableNotExistsFields,
|
||||
overallStats.nonAggregatableNotExistsFields
|
||||
);
|
||||
}
|
||||
|
||||
const nonMetricFieldsToShow = showEmptyFields ? allNonMetricFields : populatedNonMetricFields;
|
||||
|
||||
const configs: FieldVisConfig[] = [];
|
||||
|
||||
nonMetricFieldsToShow.forEach((field) => {
|
||||
const fieldData = nonMetricFieldData.find((f) => f.fieldName === field.spec.name);
|
||||
|
||||
const nonMetricConfig = {
|
||||
...(fieldData ? fieldData : {}),
|
||||
fieldFormat: currentIndexPattern.getFormatterForField(field),
|
||||
aggregatable: field.aggregatable,
|
||||
scripted: field.scripted,
|
||||
loading: fieldData?.existsInDocs,
|
||||
deletable: field.runtimeField !== undefined,
|
||||
};
|
||||
|
||||
// Map the field type from the Kibana index pattern to the field type
|
||||
// used in the data visualizer.
|
||||
const dataVisualizerType = kbnTypeToJobType(field);
|
||||
if (dataVisualizerType !== undefined) {
|
||||
nonMetricConfig.type = dataVisualizerType;
|
||||
} else {
|
||||
// Add a flag to indicate that this is one of the 'other' Kibana
|
||||
// field types that do not yet have a specific card type.
|
||||
nonMetricConfig.type = field.type;
|
||||
nonMetricConfig.isUnsupportedType = true;
|
||||
}
|
||||
|
||||
if (field.displayName !== nonMetricConfig.fieldName) {
|
||||
nonMetricConfig.displayName = field.displayName;
|
||||
}
|
||||
|
||||
configs.push(nonMetricConfig);
|
||||
});
|
||||
|
||||
setNonMetricConfigs(configs);
|
||||
}, [
|
||||
currentIndexPattern,
|
||||
dataLoader,
|
||||
indexPatternFields,
|
||||
nonMetricsLoaded,
|
||||
overallStats,
|
||||
showEmptyFields,
|
||||
]);
|
||||
|
||||
async function loadMetricFieldStats() {
|
||||
// Only request data for fields that exist in documents.
|
||||
if (metricConfigs.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const configsToLoad = metricConfigs.filter(
|
||||
(config) => config.existsInDocs === true && config.loading === true
|
||||
);
|
||||
if (configsToLoad.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pass the field name, type and cardinality in the request.
|
||||
// Top values will be obtained on a sample if cardinality > 100000.
|
||||
const existMetricFields: FieldRequestConfig[] = configsToLoad.map((config) => {
|
||||
const props = { fieldName: config.fieldName, type: config.type, cardinality: 0 };
|
||||
if (config.stats !== undefined && config.stats.cardinality !== undefined) {
|
||||
props.cardinality = config.stats.cardinality;
|
||||
}
|
||||
return props;
|
||||
});
|
||||
|
||||
// Obtain the interval to use for date histogram aggregations
|
||||
// (such as the document count chart). Aim for 75 bars.
|
||||
const buckets = getTimeBuckets();
|
||||
|
||||
const tf = timefilter as any;
|
||||
let earliest: number | undefined;
|
||||
let latest: number | undefined;
|
||||
if (currentIndexPattern.timeFieldName !== undefined) {
|
||||
earliest = tf.getActiveBounds().min.valueOf();
|
||||
latest = tf.getActiveBounds().max.valueOf();
|
||||
}
|
||||
|
||||
const bounds = tf.getActiveBounds();
|
||||
const BAR_TARGET = 75;
|
||||
buckets.setInterval('auto');
|
||||
buckets.setBounds(bounds);
|
||||
buckets.setBarTarget(BAR_TARGET);
|
||||
const aggInterval = buckets.getInterval();
|
||||
|
||||
try {
|
||||
const metricFieldStats = await dataLoader.loadFieldStats(
|
||||
searchQuery,
|
||||
samplerShardSize,
|
||||
earliest,
|
||||
latest,
|
||||
existMetricFields,
|
||||
aggInterval.asMilliseconds()
|
||||
);
|
||||
|
||||
// Add the metric stats to the existing stats in the corresponding config.
|
||||
const configs: FieldVisConfig[] = [];
|
||||
metricConfigs.forEach((config) => {
|
||||
const configWithStats = { ...config };
|
||||
if (config.fieldName !== undefined) {
|
||||
configWithStats.stats = {
|
||||
...configWithStats.stats,
|
||||
...metricFieldStats.find(
|
||||
(fieldStats: any) => fieldStats.fieldName === config.fieldName
|
||||
),
|
||||
};
|
||||
configWithStats.loading = false;
|
||||
configs.push(configWithStats);
|
||||
} else {
|
||||
// Document count card.
|
||||
configWithStats.stats = metricFieldStats.find(
|
||||
(fieldStats: any) => fieldStats.fieldName === undefined
|
||||
);
|
||||
|
||||
if (configWithStats.stats !== undefined) {
|
||||
// Add earliest / latest of timefilter for setting x axis domain.
|
||||
configWithStats.stats.timeRangeEarliest = earliest;
|
||||
configWithStats.stats.timeRangeLatest = latest;
|
||||
}
|
||||
setDocumentCountStats(configWithStats);
|
||||
}
|
||||
});
|
||||
|
||||
setMetricConfigs(configs);
|
||||
} catch (err) {
|
||||
dataLoader.displayError(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadNonMetricFieldStats() {
|
||||
// Only request data for fields that exist in documents.
|
||||
if (nonMetricConfigs.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const configsToLoad = nonMetricConfigs.filter(
|
||||
(config) => config.existsInDocs === true && config.loading === true
|
||||
);
|
||||
if (configsToLoad.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pass the field name, type and cardinality in the request.
|
||||
// Top values will be obtained on a sample if cardinality > 100000.
|
||||
const existNonMetricFields: FieldRequestConfig[] = configsToLoad.map((config) => {
|
||||
const props = { fieldName: config.fieldName, type: config.type, cardinality: 0 };
|
||||
if (config.stats !== undefined && config.stats.cardinality !== undefined) {
|
||||
props.cardinality = config.stats.cardinality;
|
||||
}
|
||||
return props;
|
||||
});
|
||||
|
||||
const tf = timefilter as any;
|
||||
let earliest;
|
||||
let latest;
|
||||
if (currentIndexPattern.timeFieldName !== undefined) {
|
||||
earliest = tf.getActiveBounds().min.valueOf();
|
||||
latest = tf.getActiveBounds().max.valueOf();
|
||||
}
|
||||
|
||||
try {
|
||||
const nonMetricFieldStats = await dataLoader.loadFieldStats(
|
||||
searchQuery,
|
||||
samplerShardSize,
|
||||
earliest,
|
||||
latest,
|
||||
existNonMetricFields
|
||||
);
|
||||
|
||||
// Add the field stats to the existing stats in the corresponding config.
|
||||
const configs: FieldVisConfig[] = [];
|
||||
nonMetricConfigs.forEach((config) => {
|
||||
const configWithStats = { ...config };
|
||||
if (config.fieldName !== undefined) {
|
||||
configWithStats.stats = {
|
||||
...configWithStats.stats,
|
||||
...nonMetricFieldStats.find(
|
||||
(fieldStats: any) => fieldStats.fieldName === config.fieldName
|
||||
),
|
||||
};
|
||||
}
|
||||
configWithStats.loading = false;
|
||||
configs.push(configWithStats);
|
||||
});
|
||||
|
||||
setNonMetricConfigs(configs);
|
||||
} catch (err) {
|
||||
dataLoader.displayError(err);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadOverallStats();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [searchQuery, samplerShardSize, lastRefresh]);
|
||||
|
||||
useEffect(() => {
|
||||
createMetricCards();
|
||||
createNonMetricCards();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [overallStats, showEmptyFields]);
|
||||
|
||||
useEffect(() => {
|
||||
loadMetricFieldStats();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [metricConfigs]);
|
||||
|
||||
useEffect(() => {
|
||||
loadNonMetricFieldStats();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [nonMetricConfigs]);
|
||||
|
||||
useEffect(() => {
|
||||
createMetricCards();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [metricsLoaded]);
|
||||
|
||||
useEffect(() => {
|
||||
createNonMetricCards();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [nonMetricsLoaded]);
|
||||
|
||||
const configs = useMemo(() => {
|
||||
let combinedConfigs = [...nonMetricConfigs, ...metricConfigs];
|
||||
if (visibleFieldTypes && visibleFieldTypes.length > 0) {
|
||||
combinedConfigs = combinedConfigs.filter(
|
||||
(config) => visibleFieldTypes.findIndex((field) => field === config.type) > -1
|
||||
);
|
||||
}
|
||||
if (visibleFieldNames && visibleFieldNames.length > 0) {
|
||||
combinedConfigs = combinedConfigs.filter(
|
||||
(config) => visibleFieldNames.findIndex((field) => field === config.fieldName) > -1
|
||||
);
|
||||
}
|
||||
|
||||
return combinedConfigs;
|
||||
}, [nonMetricConfigs, metricConfigs, visibleFieldTypes, visibleFieldNames]);
|
||||
|
||||
// Some actions open up fly-out or popup
|
||||
// This variable is used to keep track of them and clean up when unmounting
|
||||
const actionFlyoutRef = useRef<() => void | undefined>();
|
||||
useEffect(() => {
|
||||
const ref = actionFlyoutRef;
|
||||
return () => {
|
||||
// Clean up any of the flyout/editor opened from the actions
|
||||
if (ref.current) {
|
||||
ref.current();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Inject custom action column for the index based visualizer
|
||||
// Hide the column completely if no access to any of the plugins
|
||||
const extendedColumns = useMemo(() => {
|
||||
const actions = getActions(
|
||||
input.indexPattern,
|
||||
{ lens: services.lens },
|
||||
{
|
||||
searchQueryLanguage,
|
||||
searchString,
|
||||
},
|
||||
actionFlyoutRef
|
||||
);
|
||||
if (!Array.isArray(actions) || actions.length < 1) return;
|
||||
|
||||
const actionColumn: EuiTableActionsColumnType<FieldVisConfig> = {
|
||||
name: i18n.translate('xpack.dataVisualizer.index.dataGrid.actionsColumnLabel', {
|
||||
defaultMessage: 'Actions',
|
||||
}),
|
||||
actions,
|
||||
width: '70px',
|
||||
};
|
||||
|
||||
return [actionColumn];
|
||||
}, [input.indexPattern, services, searchQueryLanguage, searchString]);
|
||||
|
||||
return {
|
||||
configs,
|
||||
searchQueryLanguage,
|
||||
searchString,
|
||||
searchQuery,
|
||||
extendedColumns,
|
||||
documentCountStats,
|
||||
metricsStats,
|
||||
loaded: metricsLoaded && nonMetricsLoaded,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CoreSetup } from 'kibana/public';
|
||||
import { EmbeddableSetup } from '../../../../../../../src/plugins/embeddable/public';
|
||||
import { DataVisualizerGridEmbeddableFactory } from './grid_embeddable/grid_embeddable_factory';
|
||||
import { DataVisualizerPluginStart, DataVisualizerStartDependencies } from '../../../plugin';
|
||||
|
||||
export function registerEmbeddables(
|
||||
embeddable: EmbeddableSetup,
|
||||
core: CoreSetup<DataVisualizerStartDependencies, DataVisualizerPluginStart>
|
||||
) {
|
||||
const dataVisualizerGridEmbeddableFactory = new DataVisualizerGridEmbeddableFactory(
|
||||
core.getStartServices
|
||||
);
|
||||
embeddable.registerEmbeddableFactory(
|
||||
dataVisualizerGridEmbeddableFactory.type,
|
||||
dataVisualizerGridEmbeddableFactory
|
||||
);
|
||||
}
|
|
@ -9,7 +9,6 @@ import React, { FC, useCallback, useEffect, useState } from 'react';
|
|||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { parse, stringify } from 'query-string';
|
||||
import { isEqual } from 'lodash';
|
||||
// @ts-ignore
|
||||
import { encode } from 'rison-node';
|
||||
import { SimpleSavedObject } from 'kibana/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -29,7 +28,7 @@ import {
|
|||
isRisonSerializationRequired,
|
||||
} from '../common/util/url_state';
|
||||
import { useDataVisualizerKibana } from '../kibana_context';
|
||||
import { IndexPattern } from '../../../../../../src/plugins/data/common';
|
||||
import { DataView } from '../../../../../../src/plugins/data/common';
|
||||
import { ResultLink } from '../common/components/results_links';
|
||||
|
||||
export type IndexDataVisualizerSpec = typeof IndexDataVisualizer;
|
||||
|
@ -51,9 +50,7 @@ export const DataVisualizerUrlStateContextProvider: FC<DataVisualizerUrlStateCon
|
|||
const history = useHistory();
|
||||
const { search: searchString } = useLocation();
|
||||
|
||||
const [currentIndexPattern, setCurrentIndexPattern] = useState<IndexPattern | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [currentIndexPattern, setCurrentIndexPattern] = useState<DataView | undefined>(undefined);
|
||||
const [currentSavedSearch, setCurrentSavedSearch] = useState<SimpleSavedObject<unknown> | null>(
|
||||
null
|
||||
);
|
||||
|
|
|
@ -4,8 +4,6 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
// @ts-ignore
|
||||
import { encode } from 'rison-node';
|
||||
import { stringify } from 'query-string';
|
||||
import { SerializableRecord } from '@kbn/utility-types';
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
getQueryFromSavedSearch,
|
||||
getQueryFromSavedSearchObject,
|
||||
createMergedEsQuery,
|
||||
getEsQueryFromSavedSearch,
|
||||
} from './saved_search_utils';
|
||||
|
@ -82,9 +82,9 @@ const kqlSavedSearch: SavedSearch = {
|
|||
},
|
||||
};
|
||||
|
||||
describe('getQueryFromSavedSearch()', () => {
|
||||
describe('getQueryFromSavedSearchObject()', () => {
|
||||
it('should return parsed searchSourceJSON with query and filter', () => {
|
||||
expect(getQueryFromSavedSearch(luceneSavedSearchObj)).toEqual({
|
||||
expect(getQueryFromSavedSearchObject(luceneSavedSearchObj)).toEqual({
|
||||
filter: [
|
||||
{
|
||||
$state: { store: 'appState' },
|
||||
|
@ -106,7 +106,7 @@ describe('getQueryFromSavedSearch()', () => {
|
|||
query: { language: 'lucene', query: 'responsetime:>50' },
|
||||
version: true,
|
||||
});
|
||||
expect(getQueryFromSavedSearch(kqlSavedSearch)).toEqual({
|
||||
expect(getQueryFromSavedSearchObject(kqlSavedSearch)).toEqual({
|
||||
filter: [
|
||||
{
|
||||
$state: { store: 'appState' },
|
||||
|
@ -130,7 +130,7 @@ describe('getQueryFromSavedSearch()', () => {
|
|||
});
|
||||
});
|
||||
it('should return undefined if invalid searchSourceJSON', () => {
|
||||
expect(getQueryFromSavedSearch(luceneInvalidSavedSearchObj)).toEqual(undefined);
|
||||
expect(getQueryFromSavedSearchObject(luceneInvalidSavedSearchObj)).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -16,17 +16,31 @@ import {
|
|||
Filter,
|
||||
} from '@kbn/es-query';
|
||||
import { isSavedSearchSavedObject, SavedSearchSavedObject } from '../../../../common/types';
|
||||
import { IndexPattern } from '../../../../../../../src/plugins/data/common';
|
||||
import { IndexPattern, SearchSource } from '../../../../../../../src/plugins/data/common';
|
||||
import { SEARCH_QUERY_LANGUAGE, SearchQueryLanguage } from '../types/combined_query';
|
||||
import { SavedSearch } from '../../../../../../../src/plugins/discover/public';
|
||||
import { getEsQueryConfig } from '../../../../../../../src/plugins/data/common';
|
||||
import { FilterManager } from '../../../../../../../src/plugins/data/public';
|
||||
|
||||
const DEFAULT_QUERY = {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
match_all: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export function getDefaultQuery() {
|
||||
return cloneDeep(DEFAULT_QUERY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the stringified searchSourceJSON
|
||||
* from a saved search or saved search object
|
||||
*/
|
||||
export function getQueryFromSavedSearch(savedSearch: SavedSearchSavedObject | SavedSearch) {
|
||||
export function getQueryFromSavedSearchObject(savedSearch: SavedSearchSavedObject | SavedSearch) {
|
||||
const search = isSavedSearchSavedObject(savedSearch)
|
||||
? savedSearch?.attributes?.kibanaSavedObjectMeta
|
||||
: // @ts-expect-error kibanaSavedObjectMeta does exist
|
||||
|
@ -69,20 +83,22 @@ export function createMergedEsQuery(
|
|||
if (query.query !== '') {
|
||||
combinedQuery = toElasticsearchQuery(ast, indexPattern);
|
||||
}
|
||||
const filterQuery = buildQueryFromFilters(filters, indexPattern);
|
||||
if (combinedQuery.bool !== undefined) {
|
||||
const filterQuery = buildQueryFromFilters(filters, indexPattern);
|
||||
|
||||
if (Array.isArray(combinedQuery.bool.filter) === false) {
|
||||
combinedQuery.bool.filter =
|
||||
combinedQuery.bool.filter === undefined ? [] : [combinedQuery.bool.filter];
|
||||
if (Array.isArray(combinedQuery.bool.filter) === false) {
|
||||
combinedQuery.bool.filter =
|
||||
combinedQuery.bool.filter === undefined ? [] : [combinedQuery.bool.filter];
|
||||
}
|
||||
|
||||
if (Array.isArray(combinedQuery.bool.must_not) === false) {
|
||||
combinedQuery.bool.must_not =
|
||||
combinedQuery.bool.must_not === undefined ? [] : [combinedQuery.bool.must_not];
|
||||
}
|
||||
|
||||
combinedQuery.bool.filter = [...combinedQuery.bool.filter, ...filterQuery.filter];
|
||||
combinedQuery.bool.must_not = [...combinedQuery.bool.must_not, ...filterQuery.must_not];
|
||||
}
|
||||
|
||||
if (Array.isArray(combinedQuery.bool.must_not) === false) {
|
||||
combinedQuery.bool.must_not =
|
||||
combinedQuery.bool.must_not === undefined ? [] : [combinedQuery.bool.must_not];
|
||||
}
|
||||
|
||||
combinedQuery.bool.filter = [...combinedQuery.bool.filter, ...filterQuery.filter];
|
||||
combinedQuery.bool.must_not = [...combinedQuery.bool.must_not, ...filterQuery.must_not];
|
||||
} else {
|
||||
combinedQuery = buildEsQuery(
|
||||
indexPattern,
|
||||
|
@ -115,10 +131,31 @@ export function getEsQueryFromSavedSearch({
|
|||
}) {
|
||||
if (!indexPattern || !savedSearch) return;
|
||||
|
||||
const savedSearchData = getQueryFromSavedSearch(savedSearch);
|
||||
const userQuery = query;
|
||||
const userFilters = filters;
|
||||
|
||||
// If saved search has a search source with nested parent
|
||||
// e.g. a search coming from Dashboard saved search embeddable
|
||||
// which already combines both the saved search's original query/filters and the Dashboard's
|
||||
// then no need to process any further
|
||||
if (
|
||||
savedSearch &&
|
||||
'searchSource' in savedSearch &&
|
||||
savedSearch?.searchSource instanceof SearchSource &&
|
||||
savedSearch.searchSource.getParent() !== undefined &&
|
||||
userQuery
|
||||
) {
|
||||
return {
|
||||
searchQuery: savedSearch.searchSource.getSearchRequestBody()?.query ?? getDefaultQuery(),
|
||||
searchString: userQuery.query,
|
||||
queryLanguage: userQuery.language as SearchQueryLanguage,
|
||||
};
|
||||
}
|
||||
|
||||
// If saved search is an json object with the original query and filter
|
||||
// retrieve the parsed query and filter
|
||||
const savedSearchData = getQueryFromSavedSearchObject(savedSearch);
|
||||
|
||||
// If no saved search available, use user's query and filters
|
||||
if (!savedSearchData && userQuery) {
|
||||
if (filterManager && userFilters) filterManager.setFilters(userFilters);
|
||||
|
@ -137,7 +174,8 @@ export function getEsQueryFromSavedSearch({
|
|||
};
|
||||
}
|
||||
|
||||
// If saved search available, merge saved search with latest user query or filters differ from extracted saved search data
|
||||
// If saved search available, merge saved search with latest user query or filters
|
||||
// which might differ from extracted saved search data
|
||||
if (savedSearchData) {
|
||||
const currentQuery = userQuery ?? savedSearchData?.query;
|
||||
const currentFilters = userFilters ?? savedSearchData?.filter;
|
||||
|
@ -158,17 +196,3 @@ export function getEsQueryFromSavedSearch({
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_QUERY = {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
match_all: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export function getDefaultQuery() {
|
||||
return cloneDeep(DEFAULT_QUERY);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { CoreSetup, CoreStart } from 'kibana/public';
|
||||
import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public';
|
||||
import type { EmbeddableSetup, EmbeddableStart } from '../../../../src/plugins/embeddable/public';
|
||||
import type { SharePluginStart } from '../../../../src/plugins/share/public';
|
||||
import { Plugin } from '../../../../src/core/public';
|
||||
|
||||
|
@ -21,9 +21,11 @@ import type { IndexPatternFieldEditorStart } from '../../../../src/plugins/index
|
|||
import { getFileDataVisualizerComponent, getIndexDataVisualizerComponent } from './api';
|
||||
import { getMaxBytesFormatted } from './application/common/util/get_max_bytes';
|
||||
import { registerHomeAddData, registerHomeFeatureCatalogue } from './register_home';
|
||||
import { registerEmbeddables } from './application/index_data_visualizer/embeddables';
|
||||
|
||||
export interface DataVisualizerSetupDependencies {
|
||||
home?: HomePublicPluginSetup;
|
||||
embeddable: EmbeddableSetup;
|
||||
}
|
||||
export interface DataVisualizerStartDependencies {
|
||||
data: DataPublicPluginStart;
|
||||
|
@ -56,6 +58,9 @@ export class DataVisualizerPlugin
|
|||
registerHomeAddData(plugins.home);
|
||||
registerHomeFeatureCatalogue(plugins.home);
|
||||
}
|
||||
if (plugins.embeddable) {
|
||||
registerEmbeddables(plugins.embeddable, core);
|
||||
}
|
||||
}
|
||||
|
||||
public start(core: CoreStart, plugins: DataVisualizerStartDependencies) {
|
||||
|
|
|
@ -6,9 +6,18 @@
|
|||
"declaration": true,
|
||||
"declarationMap": true
|
||||
},
|
||||
"include": ["common/**/*", "public/**/*", "server/**/*"],
|
||||
"include": [
|
||||
"../../../typings/**/*",
|
||||
"common/**/*",
|
||||
"public/**/*",
|
||||
"scripts/**/*",
|
||||
"server/**/*",
|
||||
"types/**/*"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../../../src/core/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/kibana_utils/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/kibana_react/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/data/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/usage_collection/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/custom_integrations/tsconfig.json" },
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
import { estypes } from '@elastic/elasticsearch';
|
||||
import { useMlContext } from '../../../../../contexts/ml';
|
||||
import { SEARCH_QUERY_LANGUAGE } from '../../../../../../../common/constants/search';
|
||||
import { getQueryFromSavedSearch } from '../../../../../util/index_utils';
|
||||
import { getQueryFromSavedSearchObject } from '../../../../../util/index_utils';
|
||||
|
||||
// `undefined` is used for a non-initialized state
|
||||
// `null` is set if no saved search is used
|
||||
|
@ -40,7 +40,7 @@ export function useSavedSearch() {
|
|||
let qryString;
|
||||
|
||||
if (currentSavedSearch !== null) {
|
||||
const { query } = getQueryFromSavedSearch(currentSavedSearch);
|
||||
const { query } = getQueryFromSavedSearchObject(currentSavedSearch);
|
||||
const queryLanguage = query.language;
|
||||
qryString = query.query;
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ import { IUiSettingsClient } from 'kibana/public';
|
|||
import { getEsQueryConfig } from '../../../../../../../../src/plugins/data/public';
|
||||
import { SEARCH_QUERY_LANGUAGE } from '../../../../../common/constants/search';
|
||||
import { SavedSearchSavedObject } from '../../../../../common/types/kibana';
|
||||
import { getQueryFromSavedSearch } from '../../../util/index_utils';
|
||||
import { getQueryFromSavedSearchObject } from '../../../util/index_utils';
|
||||
|
||||
// Provider for creating the items used for searching and job creation.
|
||||
|
||||
|
@ -52,7 +52,7 @@ export function createSearchItems(
|
|||
|
||||
let combinedQuery: any = getDefaultDatafeedQuery();
|
||||
if (savedSearch !== null) {
|
||||
const data = getQueryFromSavedSearch(savedSearch);
|
||||
const data = getQueryFromSavedSearchObject(savedSearch);
|
||||
|
||||
query = data.query;
|
||||
const filter = data.filter;
|
||||
|
|
|
@ -80,7 +80,7 @@ export async function getIndexPatternAndSavedSearch(savedSearchId: string) {
|
|||
return resp;
|
||||
}
|
||||
|
||||
export function getQueryFromSavedSearch(savedSearch: SavedSearchSavedObject) {
|
||||
export function getQueryFromSavedSearchObject(savedSearch: SavedSearchSavedObject) {
|
||||
const search = savedSearch.attributes.kibanaSavedObjectMeta as { searchSourceJSON: string };
|
||||
return JSON.parse(search.searchSourceJSON) as {
|
||||
query: Query;
|
||||
|
|
|
@ -9,9 +9,10 @@ import { FtrProviderContext } from '../../../ftr_provider_context';
|
|||
|
||||
export default function ({ loadTestFile }: FtrProviderContext) {
|
||||
describe('data visualizer', function () {
|
||||
this.tags(['skipFirefox']);
|
||||
this.tags(['skipFirefox', 'mlqa']);
|
||||
|
||||
loadTestFile(require.resolve('./index_data_visualizer'));
|
||||
loadTestFile(require.resolve('./index_data_visualizer_grid_in_discover'));
|
||||
loadTestFile(require.resolve('./index_data_visualizer_actions_panel'));
|
||||
loadTestFile(require.resolve('./index_data_visualizer_index_pattern_management'));
|
||||
loadTestFile(require.resolve('./file_data_visualizer'));
|
||||
|
|
|
@ -6,374 +6,18 @@
|
|||
*/
|
||||
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
import { ML_JOB_FIELD_TYPES } from '../../../../../plugins/ml/common/constants/field_types';
|
||||
import { FieldVisConfig } from '../../../../../plugins/data_visualizer/public/application/common/components/stats_table/types';
|
||||
|
||||
interface MetricFieldVisConfig extends FieldVisConfig {
|
||||
statsMaxDecimalPlaces: number;
|
||||
docCountFormatted: string;
|
||||
topValuesCount: number;
|
||||
viewableInLens: boolean;
|
||||
}
|
||||
|
||||
interface NonMetricFieldVisConfig extends FieldVisConfig {
|
||||
docCountFormatted: string;
|
||||
exampleCount: number;
|
||||
viewableInLens: boolean;
|
||||
}
|
||||
|
||||
interface TestData {
|
||||
suiteTitle: string;
|
||||
sourceIndexOrSavedSearch: string;
|
||||
fieldNameFilters: string[];
|
||||
fieldTypeFilters: string[];
|
||||
rowsPerPage?: 10 | 25 | 50;
|
||||
sampleSizeValidations: Array<{
|
||||
size: number;
|
||||
expected: { field: string; docCountFormatted: string };
|
||||
}>;
|
||||
expected: {
|
||||
totalDocCountFormatted: string;
|
||||
metricFields?: MetricFieldVisConfig[];
|
||||
nonMetricFields?: NonMetricFieldVisConfig[];
|
||||
emptyFields: string[];
|
||||
visibleMetricFieldsCount: number;
|
||||
totalMetricFieldsCount: number;
|
||||
populatedFieldsCount: number;
|
||||
totalFieldsCount: number;
|
||||
fieldNameFiltersResultCount: number;
|
||||
fieldTypeFiltersResultCount: number;
|
||||
};
|
||||
}
|
||||
import { TestData, MetricFieldVisConfig } from './types';
|
||||
import {
|
||||
farequoteDataViewTestData,
|
||||
farequoteKQLSearchTestData,
|
||||
farequoteLuceneSearchTestData,
|
||||
sampleLogTestData,
|
||||
} from './index_test_data';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const ml = getService('ml');
|
||||
|
||||
const farequoteDataViewTestData: TestData = {
|
||||
suiteTitle: 'data view',
|
||||
sourceIndexOrSavedSearch: 'ft_farequote',
|
||||
fieldNameFilters: ['airline', '@timestamp'],
|
||||
fieldTypeFilters: [ML_JOB_FIELD_TYPES.KEYWORD],
|
||||
sampleSizeValidations: [
|
||||
{ size: 1000, expected: { field: 'airline', docCountFormatted: '1000 (100%)' } },
|
||||
{ size: 5000, expected: { field: '@timestamp', docCountFormatted: '5000 (100%)' } },
|
||||
],
|
||||
expected: {
|
||||
totalDocCountFormatted: '86,274',
|
||||
metricFields: [
|
||||
{
|
||||
fieldName: 'responsetime',
|
||||
type: ML_JOB_FIELD_TYPES.NUMBER,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
statsMaxDecimalPlaces: 3,
|
||||
topValuesCount: 10,
|
||||
viewableInLens: true,
|
||||
},
|
||||
],
|
||||
nonMetricFields: [
|
||||
{
|
||||
fieldName: '@timestamp',
|
||||
type: ML_JOB_FIELD_TYPES.DATE,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
exampleCount: 2,
|
||||
viewableInLens: true,
|
||||
},
|
||||
{
|
||||
fieldName: '@version',
|
||||
type: ML_JOB_FIELD_TYPES.TEXT,
|
||||
existsInDocs: true,
|
||||
aggregatable: false,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
docCountFormatted: '',
|
||||
viewableInLens: false,
|
||||
},
|
||||
{
|
||||
fieldName: '@version.keyword',
|
||||
type: ML_JOB_FIELD_TYPES.KEYWORD,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
viewableInLens: true,
|
||||
},
|
||||
{
|
||||
fieldName: 'airline',
|
||||
type: ML_JOB_FIELD_TYPES.KEYWORD,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
exampleCount: 10,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
viewableInLens: true,
|
||||
},
|
||||
{
|
||||
fieldName: 'type',
|
||||
type: ML_JOB_FIELD_TYPES.TEXT,
|
||||
existsInDocs: true,
|
||||
aggregatable: false,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
docCountFormatted: '',
|
||||
viewableInLens: false,
|
||||
},
|
||||
{
|
||||
fieldName: 'type.keyword',
|
||||
type: ML_JOB_FIELD_TYPES.KEYWORD,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
viewableInLens: true,
|
||||
},
|
||||
],
|
||||
emptyFields: ['sourcetype'],
|
||||
visibleMetricFieldsCount: 1,
|
||||
totalMetricFieldsCount: 1,
|
||||
populatedFieldsCount: 7,
|
||||
totalFieldsCount: 8,
|
||||
fieldNameFiltersResultCount: 2,
|
||||
fieldTypeFiltersResultCount: 3,
|
||||
},
|
||||
};
|
||||
|
||||
const farequoteKQLSearchTestData: TestData = {
|
||||
suiteTitle: 'KQL saved search',
|
||||
sourceIndexOrSavedSearch: 'ft_farequote_kuery',
|
||||
fieldNameFilters: ['@version'],
|
||||
fieldTypeFilters: [ML_JOB_FIELD_TYPES.DATE, ML_JOB_FIELD_TYPES.TEXT],
|
||||
sampleSizeValidations: [
|
||||
{ size: 1000, expected: { field: 'airline', docCountFormatted: '1000 (100%)' } },
|
||||
{ size: 5000, expected: { field: '@timestamp', docCountFormatted: '5000 (100%)' } },
|
||||
],
|
||||
expected: {
|
||||
totalDocCountFormatted: '34,415',
|
||||
metricFields: [
|
||||
{
|
||||
fieldName: 'responsetime',
|
||||
type: ML_JOB_FIELD_TYPES.NUMBER,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
statsMaxDecimalPlaces: 3,
|
||||
topValuesCount: 10,
|
||||
viewableInLens: true,
|
||||
},
|
||||
],
|
||||
nonMetricFields: [
|
||||
{
|
||||
fieldName: '@timestamp',
|
||||
type: ML_JOB_FIELD_TYPES.DATE,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
exampleCount: 2,
|
||||
viewableInLens: true,
|
||||
},
|
||||
{
|
||||
fieldName: '@version',
|
||||
type: ML_JOB_FIELD_TYPES.TEXT,
|
||||
existsInDocs: true,
|
||||
aggregatable: false,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
docCountFormatted: '',
|
||||
viewableInLens: false,
|
||||
},
|
||||
{
|
||||
fieldName: '@version.keyword',
|
||||
type: ML_JOB_FIELD_TYPES.KEYWORD,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
viewableInLens: true,
|
||||
},
|
||||
{
|
||||
fieldName: 'airline',
|
||||
type: ML_JOB_FIELD_TYPES.KEYWORD,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
exampleCount: 5,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
viewableInLens: true,
|
||||
},
|
||||
{
|
||||
fieldName: 'type',
|
||||
type: ML_JOB_FIELD_TYPES.TEXT,
|
||||
existsInDocs: true,
|
||||
aggregatable: false,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
docCountFormatted: '',
|
||||
viewableInLens: false,
|
||||
},
|
||||
{
|
||||
fieldName: 'type.keyword',
|
||||
type: ML_JOB_FIELD_TYPES.KEYWORD,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
viewableInLens: true,
|
||||
},
|
||||
],
|
||||
emptyFields: ['sourcetype'],
|
||||
visibleMetricFieldsCount: 1,
|
||||
totalMetricFieldsCount: 1,
|
||||
populatedFieldsCount: 7,
|
||||
totalFieldsCount: 8,
|
||||
fieldNameFiltersResultCount: 1,
|
||||
fieldTypeFiltersResultCount: 3,
|
||||
},
|
||||
};
|
||||
|
||||
const farequoteLuceneSearchTestData: TestData = {
|
||||
suiteTitle: 'lucene saved search',
|
||||
sourceIndexOrSavedSearch: 'ft_farequote_lucene',
|
||||
fieldNameFilters: ['@version.keyword', 'type'],
|
||||
fieldTypeFilters: [ML_JOB_FIELD_TYPES.NUMBER],
|
||||
sampleSizeValidations: [
|
||||
{ size: 1000, expected: { field: 'airline', docCountFormatted: '1000 (100%)' } },
|
||||
{ size: 5000, expected: { field: '@timestamp', docCountFormatted: '5000 (100%)' } },
|
||||
],
|
||||
expected: {
|
||||
totalDocCountFormatted: '34,416',
|
||||
metricFields: [
|
||||
{
|
||||
fieldName: 'responsetime',
|
||||
type: ML_JOB_FIELD_TYPES.NUMBER,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
statsMaxDecimalPlaces: 3,
|
||||
topValuesCount: 10,
|
||||
viewableInLens: true,
|
||||
},
|
||||
],
|
||||
nonMetricFields: [
|
||||
{
|
||||
fieldName: '@timestamp',
|
||||
type: ML_JOB_FIELD_TYPES.DATE,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
exampleCount: 2,
|
||||
viewableInLens: true,
|
||||
},
|
||||
{
|
||||
fieldName: '@version',
|
||||
type: ML_JOB_FIELD_TYPES.TEXT,
|
||||
existsInDocs: true,
|
||||
aggregatable: false,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
docCountFormatted: '',
|
||||
viewableInLens: false,
|
||||
},
|
||||
{
|
||||
fieldName: '@version.keyword',
|
||||
type: ML_JOB_FIELD_TYPES.KEYWORD,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
viewableInLens: true,
|
||||
},
|
||||
{
|
||||
fieldName: 'airline',
|
||||
type: ML_JOB_FIELD_TYPES.KEYWORD,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
exampleCount: 5,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
viewableInLens: true,
|
||||
},
|
||||
{
|
||||
fieldName: 'type',
|
||||
type: ML_JOB_FIELD_TYPES.TEXT,
|
||||
existsInDocs: true,
|
||||
aggregatable: false,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
docCountFormatted: '',
|
||||
viewableInLens: false,
|
||||
},
|
||||
{
|
||||
fieldName: 'type.keyword',
|
||||
type: ML_JOB_FIELD_TYPES.KEYWORD,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
viewableInLens: true,
|
||||
},
|
||||
],
|
||||
emptyFields: ['sourcetype'],
|
||||
visibleMetricFieldsCount: 1,
|
||||
totalMetricFieldsCount: 1,
|
||||
populatedFieldsCount: 7,
|
||||
totalFieldsCount: 8,
|
||||
fieldNameFiltersResultCount: 2,
|
||||
fieldTypeFiltersResultCount: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const sampleLogTestData: TestData = {
|
||||
suiteTitle: 'geo point field',
|
||||
sourceIndexOrSavedSearch: 'ft_module_sample_logs',
|
||||
fieldNameFilters: ['geo.coordinates'],
|
||||
fieldTypeFilters: [ML_JOB_FIELD_TYPES.GEO_POINT],
|
||||
rowsPerPage: 50,
|
||||
expected: {
|
||||
totalDocCountFormatted: '408',
|
||||
metricFields: [],
|
||||
// only testing the geo_point fields
|
||||
nonMetricFields: [
|
||||
{
|
||||
fieldName: 'geo.coordinates',
|
||||
type: ML_JOB_FIELD_TYPES.GEO_POINT,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
docCountFormatted: '408 (100%)',
|
||||
exampleCount: 10,
|
||||
viewableInLens: false,
|
||||
},
|
||||
],
|
||||
emptyFields: [],
|
||||
visibleMetricFieldsCount: 4,
|
||||
totalMetricFieldsCount: 5,
|
||||
populatedFieldsCount: 35,
|
||||
totalFieldsCount: 36,
|
||||
fieldNameFiltersResultCount: 1,
|
||||
fieldTypeFiltersResultCount: 1,
|
||||
},
|
||||
sampleSizeValidations: [
|
||||
{ size: 1000, expected: { field: 'geo.coordinates', docCountFormatted: '408 (100%)' } },
|
||||
{ size: 5000, expected: { field: '@timestamp', docCountFormatted: '408 (100%)' } },
|
||||
],
|
||||
};
|
||||
|
||||
function runTests(testData: TestData) {
|
||||
it(`${testData.suiteTitle} loads the source data in the data visualizer`, async () => {
|
||||
await ml.testExecution.logTestStep(
|
||||
|
@ -541,7 +185,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
describe('with module_sample_logs ', function () {
|
||||
// Run tests on full farequote index.
|
||||
// Run tests on full ft_module_sample_logs index.
|
||||
it(`${sampleLogTestData.suiteTitle} loads the data visualizer selector page`, async () => {
|
||||
// Start navigation from the base of the ML app.
|
||||
await ml.navigation.navigateToMl();
|
||||
|
|
|
@ -0,0 +1,172 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
import { TestData, MetricFieldVisConfig } from './types';
|
||||
|
||||
const SHOW_FIELD_STATISTICS = 'discover:showFieldStatistics';
|
||||
import {
|
||||
farequoteDataViewTestData,
|
||||
farequoteKQLSearchTestData,
|
||||
farequoteLuceneFiltersSearchTestData,
|
||||
farequoteKQLFiltersSearchTestData,
|
||||
farequoteLuceneSearchTestData,
|
||||
sampleLogTestData,
|
||||
} from './index_test_data';
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const PageObjects = getPageObjects(['common', 'discover', 'timePicker', 'settings']);
|
||||
const ml = getService('ml');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const retry = getService('retry');
|
||||
const toasts = getService('toasts');
|
||||
|
||||
const selectIndexPattern = async (indexPattern: string) => {
|
||||
await retry.tryForTime(2 * 1000, async () => {
|
||||
await PageObjects.discover.selectIndexPattern(indexPattern);
|
||||
const indexPatternTitle = await testSubjects.getVisibleText('indexPattern-switch-link');
|
||||
expect(indexPatternTitle).to.be(indexPattern);
|
||||
});
|
||||
};
|
||||
|
||||
const clearAdvancedSetting = async (propertyName: string) => {
|
||||
await retry.tryForTime(2 * 1000, async () => {
|
||||
await PageObjects.common.navigateToUrl('management', 'kibana/settings', {
|
||||
shouldUseHashForSubUrl: false,
|
||||
});
|
||||
if ((await PageObjects.settings.getAdvancedSettingCheckbox(propertyName)) === 'true') {
|
||||
await PageObjects.settings.clearAdvancedSettings(propertyName);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const setAdvancedSettingCheckbox = async (propertyName: string, checkedState: boolean) => {
|
||||
await retry.tryForTime(2 * 1000, async () => {
|
||||
await PageObjects.common.navigateToUrl('management', 'kibana/settings', {
|
||||
shouldUseHashForSubUrl: false,
|
||||
});
|
||||
await testSubjects.click('settings');
|
||||
await toasts.dismissAllToasts();
|
||||
await PageObjects.settings.toggleAdvancedSettingCheckbox(propertyName, checkedState);
|
||||
});
|
||||
};
|
||||
|
||||
function runTestsWhenDisabled(testData: TestData) {
|
||||
it('should not show view mode toggle or Field stats table', async function () {
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
if (testData.isSavedSearch) {
|
||||
await retry.tryForTime(2 * 1000, async () => {
|
||||
await PageObjects.discover.loadSavedSearch(testData.sourceIndexOrSavedSearch);
|
||||
});
|
||||
} else {
|
||||
await selectIndexPattern(testData.sourceIndexOrSavedSearch);
|
||||
}
|
||||
|
||||
await PageObjects.timePicker.setAbsoluteRange(
|
||||
'Jan 1, 2016 @ 00:00:00.000',
|
||||
'Nov 1, 2020 @ 00:00:00.000'
|
||||
);
|
||||
|
||||
await PageObjects.discover.assertViewModeToggleNotExists();
|
||||
await PageObjects.discover.assertFieldStatsTableNotExists();
|
||||
});
|
||||
}
|
||||
|
||||
function runTests(testData: TestData) {
|
||||
describe(`with ${testData.suiteTitle}`, function () {
|
||||
it(`displays the 'Field statistics' table content correctly`, async function () {
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
if (testData.isSavedSearch) {
|
||||
await retry.tryForTime(2 * 1000, async () => {
|
||||
await PageObjects.discover.loadSavedSearch(testData.sourceIndexOrSavedSearch);
|
||||
});
|
||||
} else {
|
||||
await selectIndexPattern(testData.sourceIndexOrSavedSearch);
|
||||
}
|
||||
await PageObjects.timePicker.setAbsoluteRange(
|
||||
'Jan 1, 2016 @ 00:00:00.000',
|
||||
'Nov 1, 2020 @ 00:00:00.000'
|
||||
);
|
||||
|
||||
await PageObjects.discover.assertHitCount(testData.expected.totalDocCountFormatted);
|
||||
await PageObjects.discover.assertViewModeToggleExists();
|
||||
await PageObjects.discover.clickViewModeFieldStatsButton();
|
||||
await ml.testExecution.logTestStep(
|
||||
'displays details for metric fields and non-metric fields correctly'
|
||||
);
|
||||
for (const fieldRow of testData.expected.metricFields as Array<
|
||||
Required<MetricFieldVisConfig>
|
||||
>) {
|
||||
await ml.dataVisualizerTable.assertNumberFieldContents(
|
||||
fieldRow.fieldName,
|
||||
fieldRow.docCountFormatted,
|
||||
fieldRow.topValuesCount,
|
||||
fieldRow.viewableInLens
|
||||
);
|
||||
}
|
||||
|
||||
for (const fieldRow of testData.expected.nonMetricFields!) {
|
||||
await ml.dataVisualizerTable.assertNonMetricFieldContents(
|
||||
fieldRow.type,
|
||||
fieldRow.fieldName!,
|
||||
fieldRow.docCountFormatted,
|
||||
fieldRow.exampleCount,
|
||||
fieldRow.viewableInLens,
|
||||
false,
|
||||
fieldRow.exampleContent
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('field statistics in Discover', function () {
|
||||
before(async function () {
|
||||
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote');
|
||||
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/module_sample_logs');
|
||||
await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp');
|
||||
await ml.testResources.createIndexPatternIfNeeded('ft_module_sample_logs', '@timestamp');
|
||||
await ml.testResources.createSavedSearchFarequoteKueryIfNeeded();
|
||||
await ml.testResources.createSavedSearchFarequoteLuceneIfNeeded();
|
||||
await ml.testResources.createSavedSearchFarequoteFilterAndLuceneIfNeeded();
|
||||
await ml.testResources.createSavedSearchFarequoteFilterAndKueryIfNeeded();
|
||||
|
||||
await ml.securityUI.loginAsMlPowerUser();
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await clearAdvancedSetting(SHOW_FIELD_STATISTICS);
|
||||
});
|
||||
|
||||
describe('when enabled', function () {
|
||||
before(async function () {
|
||||
await setAdvancedSettingCheckbox(SHOW_FIELD_STATISTICS, true);
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await clearAdvancedSetting(SHOW_FIELD_STATISTICS);
|
||||
});
|
||||
|
||||
runTests(farequoteDataViewTestData);
|
||||
runTests(farequoteKQLSearchTestData);
|
||||
runTests(farequoteLuceneSearchTestData);
|
||||
runTests(farequoteKQLFiltersSearchTestData);
|
||||
runTests(farequoteLuceneFiltersSearchTestData);
|
||||
runTests(sampleLogTestData);
|
||||
});
|
||||
|
||||
describe('when disabled', function () {
|
||||
before(async function () {
|
||||
// Ensure that the setting is set to default state which is false
|
||||
await setAdvancedSettingCheckbox(SHOW_FIELD_STATISTICS, false);
|
||||
});
|
||||
|
||||
runTestsWhenDisabled(farequoteDataViewTestData);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,533 @@
|
|||
/*
|
||||
* 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 { TestData } from './types';
|
||||
import { ML_JOB_FIELD_TYPES } from '../../../../../plugins/ml/common/constants/field_types';
|
||||
|
||||
export const farequoteDataViewTestData: TestData = {
|
||||
suiteTitle: 'farequote index pattern',
|
||||
isSavedSearch: false,
|
||||
sourceIndexOrSavedSearch: 'ft_farequote',
|
||||
fieldNameFilters: ['airline', '@timestamp'],
|
||||
fieldTypeFilters: [ML_JOB_FIELD_TYPES.KEYWORD],
|
||||
sampleSizeValidations: [
|
||||
{ size: 1000, expected: { field: 'airline', docCountFormatted: '1000 (100%)' } },
|
||||
{ size: 5000, expected: { field: '@timestamp', docCountFormatted: '5000 (100%)' } },
|
||||
],
|
||||
expected: {
|
||||
totalDocCountFormatted: '86,274',
|
||||
metricFields: [
|
||||
{
|
||||
fieldName: 'responsetime',
|
||||
type: ML_JOB_FIELD_TYPES.NUMBER,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
statsMaxDecimalPlaces: 3,
|
||||
topValuesCount: 10,
|
||||
viewableInLens: true,
|
||||
},
|
||||
],
|
||||
nonMetricFields: [
|
||||
{
|
||||
fieldName: '@timestamp',
|
||||
type: ML_JOB_FIELD_TYPES.DATE,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
exampleCount: 2,
|
||||
viewableInLens: true,
|
||||
},
|
||||
{
|
||||
fieldName: '@version',
|
||||
type: ML_JOB_FIELD_TYPES.TEXT,
|
||||
existsInDocs: true,
|
||||
aggregatable: false,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
docCountFormatted: '',
|
||||
viewableInLens: false,
|
||||
},
|
||||
{
|
||||
fieldName: '@version.keyword',
|
||||
type: ML_JOB_FIELD_TYPES.KEYWORD,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
viewableInLens: true,
|
||||
},
|
||||
{
|
||||
fieldName: 'airline',
|
||||
type: ML_JOB_FIELD_TYPES.KEYWORD,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
exampleCount: 10,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
viewableInLens: true,
|
||||
},
|
||||
{
|
||||
fieldName: 'type',
|
||||
type: ML_JOB_FIELD_TYPES.TEXT,
|
||||
existsInDocs: true,
|
||||
aggregatable: false,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
docCountFormatted: '',
|
||||
viewableInLens: false,
|
||||
},
|
||||
{
|
||||
fieldName: 'type.keyword',
|
||||
type: ML_JOB_FIELD_TYPES.KEYWORD,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
viewableInLens: true,
|
||||
},
|
||||
],
|
||||
emptyFields: ['sourcetype'],
|
||||
visibleMetricFieldsCount: 1,
|
||||
totalMetricFieldsCount: 1,
|
||||
populatedFieldsCount: 7,
|
||||
totalFieldsCount: 8,
|
||||
fieldNameFiltersResultCount: 2,
|
||||
fieldTypeFiltersResultCount: 3,
|
||||
},
|
||||
};
|
||||
|
||||
export const farequoteKQLSearchTestData: TestData = {
|
||||
suiteTitle: 'KQL saved search',
|
||||
isSavedSearch: true,
|
||||
sourceIndexOrSavedSearch: 'ft_farequote_kuery',
|
||||
fieldNameFilters: ['@version'],
|
||||
fieldTypeFilters: [ML_JOB_FIELD_TYPES.DATE, ML_JOB_FIELD_TYPES.TEXT],
|
||||
sampleSizeValidations: [
|
||||
{ size: 1000, expected: { field: 'airline', docCountFormatted: '1000 (100%)' } },
|
||||
{ size: 5000, expected: { field: '@timestamp', docCountFormatted: '5000 (100%)' } },
|
||||
],
|
||||
expected: {
|
||||
totalDocCountFormatted: '34,415',
|
||||
metricFields: [
|
||||
{
|
||||
fieldName: 'responsetime',
|
||||
type: ML_JOB_FIELD_TYPES.NUMBER,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
statsMaxDecimalPlaces: 3,
|
||||
topValuesCount: 10,
|
||||
viewableInLens: true,
|
||||
},
|
||||
],
|
||||
nonMetricFields: [
|
||||
{
|
||||
fieldName: '@timestamp',
|
||||
type: ML_JOB_FIELD_TYPES.DATE,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
exampleCount: 2,
|
||||
viewableInLens: true,
|
||||
},
|
||||
{
|
||||
fieldName: '@version',
|
||||
type: ML_JOB_FIELD_TYPES.TEXT,
|
||||
existsInDocs: true,
|
||||
aggregatable: false,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
docCountFormatted: '',
|
||||
viewableInLens: false,
|
||||
},
|
||||
{
|
||||
fieldName: '@version.keyword',
|
||||
type: ML_JOB_FIELD_TYPES.KEYWORD,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
viewableInLens: true,
|
||||
},
|
||||
{
|
||||
fieldName: 'airline',
|
||||
type: ML_JOB_FIELD_TYPES.KEYWORD,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
exampleCount: 5,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
viewableInLens: true,
|
||||
},
|
||||
{
|
||||
fieldName: 'type',
|
||||
type: ML_JOB_FIELD_TYPES.TEXT,
|
||||
existsInDocs: true,
|
||||
aggregatable: false,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
docCountFormatted: '',
|
||||
viewableInLens: false,
|
||||
},
|
||||
{
|
||||
fieldName: 'type.keyword',
|
||||
type: ML_JOB_FIELD_TYPES.KEYWORD,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
viewableInLens: true,
|
||||
},
|
||||
],
|
||||
emptyFields: ['sourcetype'],
|
||||
visibleMetricFieldsCount: 1,
|
||||
totalMetricFieldsCount: 1,
|
||||
populatedFieldsCount: 7,
|
||||
totalFieldsCount: 8,
|
||||
fieldNameFiltersResultCount: 1,
|
||||
fieldTypeFiltersResultCount: 3,
|
||||
},
|
||||
};
|
||||
|
||||
export const farequoteKQLFiltersSearchTestData: TestData = {
|
||||
suiteTitle: 'KQL saved search and filters',
|
||||
isSavedSearch: true,
|
||||
sourceIndexOrSavedSearch: 'ft_farequote_filter_and_kuery',
|
||||
fieldNameFilters: ['@version'],
|
||||
fieldTypeFilters: [ML_JOB_FIELD_TYPES.DATE, ML_JOB_FIELD_TYPES.TEXT],
|
||||
sampleSizeValidations: [
|
||||
{ size: 1000, expected: { field: 'airline', docCountFormatted: '1000 (100%)' } },
|
||||
{ size: 5000, expected: { field: '@timestamp', docCountFormatted: '5000 (100%)' } },
|
||||
],
|
||||
expected: {
|
||||
totalDocCountFormatted: '5,674',
|
||||
metricFields: [
|
||||
{
|
||||
fieldName: 'responsetime',
|
||||
type: ML_JOB_FIELD_TYPES.NUMBER,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
statsMaxDecimalPlaces: 3,
|
||||
topValuesCount: 10,
|
||||
viewableInLens: true,
|
||||
},
|
||||
],
|
||||
nonMetricFields: [
|
||||
{
|
||||
fieldName: '@timestamp',
|
||||
type: ML_JOB_FIELD_TYPES.DATE,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
exampleCount: 2,
|
||||
viewableInLens: true,
|
||||
},
|
||||
{
|
||||
fieldName: '@version',
|
||||
type: ML_JOB_FIELD_TYPES.TEXT,
|
||||
existsInDocs: true,
|
||||
aggregatable: false,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
docCountFormatted: '',
|
||||
viewableInLens: false,
|
||||
},
|
||||
{
|
||||
fieldName: '@version.keyword',
|
||||
type: ML_JOB_FIELD_TYPES.KEYWORD,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
viewableInLens: true,
|
||||
},
|
||||
{
|
||||
fieldName: 'airline',
|
||||
type: ML_JOB_FIELD_TYPES.KEYWORD,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
exampleContent: ['ASA'],
|
||||
docCountFormatted: '5000 (100%)',
|
||||
viewableInLens: true,
|
||||
},
|
||||
{
|
||||
fieldName: 'type',
|
||||
type: ML_JOB_FIELD_TYPES.TEXT,
|
||||
existsInDocs: true,
|
||||
aggregatable: false,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
docCountFormatted: '',
|
||||
viewableInLens: false,
|
||||
},
|
||||
{
|
||||
fieldName: 'type.keyword',
|
||||
type: ML_JOB_FIELD_TYPES.KEYWORD,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
viewableInLens: true,
|
||||
},
|
||||
],
|
||||
emptyFields: ['sourcetype'],
|
||||
visibleMetricFieldsCount: 1,
|
||||
totalMetricFieldsCount: 1,
|
||||
populatedFieldsCount: 7,
|
||||
totalFieldsCount: 8,
|
||||
fieldNameFiltersResultCount: 1,
|
||||
fieldTypeFiltersResultCount: 3,
|
||||
},
|
||||
};
|
||||
|
||||
export const farequoteLuceneSearchTestData: TestData = {
|
||||
suiteTitle: 'lucene saved search',
|
||||
isSavedSearch: true,
|
||||
sourceIndexOrSavedSearch: 'ft_farequote_lucene',
|
||||
fieldNameFilters: ['@version.keyword', 'type'],
|
||||
fieldTypeFilters: [ML_JOB_FIELD_TYPES.NUMBER],
|
||||
sampleSizeValidations: [
|
||||
{ size: 1000, expected: { field: 'airline', docCountFormatted: '1000 (100%)' } },
|
||||
{ size: 5000, expected: { field: '@timestamp', docCountFormatted: '5000 (100%)' } },
|
||||
],
|
||||
expected: {
|
||||
totalDocCountFormatted: '34,416',
|
||||
metricFields: [
|
||||
{
|
||||
fieldName: 'responsetime',
|
||||
type: ML_JOB_FIELD_TYPES.NUMBER,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
statsMaxDecimalPlaces: 3,
|
||||
topValuesCount: 10,
|
||||
viewableInLens: true,
|
||||
},
|
||||
],
|
||||
nonMetricFields: [
|
||||
{
|
||||
fieldName: '@timestamp',
|
||||
type: ML_JOB_FIELD_TYPES.DATE,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
exampleCount: 2,
|
||||
viewableInLens: true,
|
||||
},
|
||||
{
|
||||
fieldName: '@version',
|
||||
type: ML_JOB_FIELD_TYPES.TEXT,
|
||||
existsInDocs: true,
|
||||
aggregatable: false,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
docCountFormatted: '',
|
||||
viewableInLens: false,
|
||||
},
|
||||
{
|
||||
fieldName: '@version.keyword',
|
||||
type: ML_JOB_FIELD_TYPES.KEYWORD,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
viewableInLens: true,
|
||||
},
|
||||
{
|
||||
fieldName: 'airline',
|
||||
type: ML_JOB_FIELD_TYPES.KEYWORD,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
exampleCount: 5,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
viewableInLens: true,
|
||||
},
|
||||
{
|
||||
fieldName: 'type',
|
||||
type: ML_JOB_FIELD_TYPES.TEXT,
|
||||
existsInDocs: true,
|
||||
aggregatable: false,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
docCountFormatted: '',
|
||||
viewableInLens: false,
|
||||
},
|
||||
{
|
||||
fieldName: 'type.keyword',
|
||||
type: ML_JOB_FIELD_TYPES.KEYWORD,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
viewableInLens: true,
|
||||
},
|
||||
],
|
||||
emptyFields: ['sourcetype'],
|
||||
visibleMetricFieldsCount: 1,
|
||||
totalMetricFieldsCount: 1,
|
||||
populatedFieldsCount: 7,
|
||||
totalFieldsCount: 8,
|
||||
fieldNameFiltersResultCount: 2,
|
||||
fieldTypeFiltersResultCount: 1,
|
||||
},
|
||||
};
|
||||
|
||||
export const farequoteLuceneFiltersSearchTestData: TestData = {
|
||||
suiteTitle: 'lucene saved search and filter',
|
||||
isSavedSearch: true,
|
||||
sourceIndexOrSavedSearch: 'ft_farequote_filter_and_lucene',
|
||||
fieldNameFilters: ['@version.keyword', 'type'],
|
||||
fieldTypeFilters: [ML_JOB_FIELD_TYPES.NUMBER],
|
||||
sampleSizeValidations: [
|
||||
{ size: 1000, expected: { field: 'airline', docCountFormatted: '1000 (100%)' } },
|
||||
{ size: 5000, expected: { field: '@timestamp', docCountFormatted: '5000 (100%)' } },
|
||||
],
|
||||
expected: {
|
||||
totalDocCountFormatted: '5,673',
|
||||
metricFields: [
|
||||
{
|
||||
fieldName: 'responsetime',
|
||||
type: ML_JOB_FIELD_TYPES.NUMBER,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
statsMaxDecimalPlaces: 3,
|
||||
topValuesCount: 10,
|
||||
viewableInLens: true,
|
||||
},
|
||||
],
|
||||
nonMetricFields: [
|
||||
{
|
||||
fieldName: '@timestamp',
|
||||
type: ML_JOB_FIELD_TYPES.DATE,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
exampleCount: 2,
|
||||
viewableInLens: true,
|
||||
},
|
||||
{
|
||||
fieldName: '@version',
|
||||
type: ML_JOB_FIELD_TYPES.TEXT,
|
||||
existsInDocs: true,
|
||||
aggregatable: false,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
docCountFormatted: '',
|
||||
viewableInLens: false,
|
||||
},
|
||||
{
|
||||
fieldName: '@version.keyword',
|
||||
type: ML_JOB_FIELD_TYPES.KEYWORD,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
viewableInLens: true,
|
||||
},
|
||||
{
|
||||
fieldName: 'airline',
|
||||
type: ML_JOB_FIELD_TYPES.KEYWORD,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
exampleContent: ['ASA'],
|
||||
docCountFormatted: '5000 (100%)',
|
||||
viewableInLens: true,
|
||||
},
|
||||
{
|
||||
fieldName: 'type',
|
||||
type: ML_JOB_FIELD_TYPES.TEXT,
|
||||
existsInDocs: true,
|
||||
aggregatable: false,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
docCountFormatted: '',
|
||||
viewableInLens: false,
|
||||
},
|
||||
{
|
||||
fieldName: 'type.keyword',
|
||||
type: ML_JOB_FIELD_TYPES.KEYWORD,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
exampleCount: 1,
|
||||
docCountFormatted: '5000 (100%)',
|
||||
viewableInLens: true,
|
||||
},
|
||||
],
|
||||
emptyFields: ['sourcetype'],
|
||||
visibleMetricFieldsCount: 1,
|
||||
totalMetricFieldsCount: 1,
|
||||
populatedFieldsCount: 7,
|
||||
totalFieldsCount: 8,
|
||||
fieldNameFiltersResultCount: 2,
|
||||
fieldTypeFiltersResultCount: 1,
|
||||
},
|
||||
};
|
||||
|
||||
export const sampleLogTestData: TestData = {
|
||||
suiteTitle: 'geo point field',
|
||||
isSavedSearch: false,
|
||||
sourceIndexOrSavedSearch: 'ft_module_sample_logs',
|
||||
fieldNameFilters: ['geo.coordinates'],
|
||||
fieldTypeFilters: [ML_JOB_FIELD_TYPES.GEO_POINT],
|
||||
rowsPerPage: 50,
|
||||
expected: {
|
||||
totalDocCountFormatted: '408',
|
||||
metricFields: [],
|
||||
// only testing the geo_point fields
|
||||
nonMetricFields: [
|
||||
{
|
||||
fieldName: 'geo.coordinates',
|
||||
type: ML_JOB_FIELD_TYPES.GEO_POINT,
|
||||
existsInDocs: true,
|
||||
aggregatable: true,
|
||||
loading: false,
|
||||
docCountFormatted: '408 (100%)',
|
||||
exampleCount: 10,
|
||||
viewableInLens: false,
|
||||
},
|
||||
],
|
||||
emptyFields: [],
|
||||
visibleMetricFieldsCount: 4,
|
||||
totalMetricFieldsCount: 5,
|
||||
populatedFieldsCount: 35,
|
||||
totalFieldsCount: 36,
|
||||
fieldNameFiltersResultCount: 1,
|
||||
fieldTypeFiltersResultCount: 1,
|
||||
},
|
||||
sampleSizeValidations: [
|
||||
{ size: 1000, expected: { field: 'geo.coordinates', docCountFormatted: '408 (100%)' } },
|
||||
{ size: 5000, expected: { field: '@timestamp', docCountFormatted: '408 (100%)' } },
|
||||
],
|
||||
};
|
47
x-pack/test/functional/apps/ml/data_visualizer/types.ts
Normal file
47
x-pack/test/functional/apps/ml/data_visualizer/types.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 { FieldVisConfig } from '../../../../../plugins/data_visualizer/public/application/common/components/stats_table/types';
|
||||
|
||||
export interface MetricFieldVisConfig extends FieldVisConfig {
|
||||
statsMaxDecimalPlaces: number;
|
||||
docCountFormatted: string;
|
||||
topValuesCount: number;
|
||||
viewableInLens: boolean;
|
||||
}
|
||||
|
||||
export interface NonMetricFieldVisConfig extends FieldVisConfig {
|
||||
docCountFormatted: string;
|
||||
exampleCount: number;
|
||||
exampleContent?: string[];
|
||||
viewableInLens: boolean;
|
||||
}
|
||||
|
||||
export interface TestData {
|
||||
suiteTitle: string;
|
||||
isSavedSearch?: boolean;
|
||||
sourceIndexOrSavedSearch: string;
|
||||
fieldNameFilters: string[];
|
||||
fieldTypeFilters: string[];
|
||||
rowsPerPage?: 10 | 25 | 50;
|
||||
sampleSizeValidations: Array<{
|
||||
size: number;
|
||||
expected: { field: string; docCountFormatted: string };
|
||||
}>;
|
||||
expected: {
|
||||
totalDocCountFormatted: string;
|
||||
metricFields?: MetricFieldVisConfig[];
|
||||
nonMetricFields?: NonMetricFieldVisConfig[];
|
||||
emptyFields: string[];
|
||||
visibleMetricFieldsCount: number;
|
||||
totalMetricFieldsCount: number;
|
||||
populatedFieldsCount: number;
|
||||
totalFieldsCount: number;
|
||||
fieldNameFiltersResultCount: number;
|
||||
fieldTypeFiltersResultCount: number;
|
||||
};
|
||||
}
|
|
@ -169,7 +169,10 @@ export function MachineLearningCustomUrlsProvider({
|
|||
|
||||
async assertDiscoverCustomUrlAction(expectedHitCountFormatted: string) {
|
||||
await PageObjects.discover.waitForDiscoverAppOnScreen();
|
||||
await retry.tryForTime(5000, async () => {
|
||||
// During cloud tests, the small browser width might cause hit count to be invisible
|
||||
// so temporarily collapsing the sidebar ensures the count shows
|
||||
await PageObjects.discover.closeSidebar();
|
||||
await retry.tryForTime(10 * 1000, async () => {
|
||||
const hitCount = await PageObjects.discover.getHitCount();
|
||||
expect(hitCount).to.eql(
|
||||
expectedHitCountFormatted,
|
||||
|
|
|
@ -361,7 +361,27 @@ export function MachineLearningDataVisualizerTableProvider(
|
|||
});
|
||||
}
|
||||
|
||||
public async assertTopValuesContents(fieldName: string, expectedTopValuesCount: number) {
|
||||
public async assertTopValuesContent(fieldName: string, expectedTopValues: string[]) {
|
||||
const selector = this.detailsSelector(fieldName, 'dataVisualizerFieldDataTopValuesContent');
|
||||
const topValuesElement = await testSubjects.find(selector);
|
||||
const topValuesBars = await topValuesElement.findAllByTestSubject(
|
||||
'dataVisualizerFieldDataTopValueBar'
|
||||
);
|
||||
|
||||
const topValuesBarsValues = await Promise.all(
|
||||
topValuesBars.map(async (bar) => {
|
||||
const visibleText = await bar.getVisibleText();
|
||||
return visibleText ? visibleText.split('\n')[0] : undefined;
|
||||
})
|
||||
);
|
||||
|
||||
expect(topValuesBarsValues).to.eql(
|
||||
expectedTopValues,
|
||||
`Expected top values for field '${fieldName}' to equal '${expectedTopValues}' (got '${topValuesBarsValues}')`
|
||||
);
|
||||
}
|
||||
|
||||
public async assertTopValuesCount(fieldName: string, expectedTopValuesCount: number) {
|
||||
const selector = this.detailsSelector(fieldName, 'dataVisualizerFieldDataTopValuesContent');
|
||||
const topValuesElement = await testSubjects.find(selector);
|
||||
const topValuesBars = await topValuesElement.findAllByTestSubject(
|
||||
|
@ -401,7 +421,7 @@ export function MachineLearningDataVisualizerTableProvider(
|
|||
await testSubjects.existOrFail(
|
||||
this.detailsSelector(fieldName, 'dataVisualizerFieldDataTopValues')
|
||||
);
|
||||
await this.assertTopValuesContents(fieldName, topValuesCount);
|
||||
await this.assertTopValuesCount(fieldName, topValuesCount);
|
||||
|
||||
if (checkDistributionPreviewExist) {
|
||||
await this.assertDistributionPreviewExist(fieldName);
|
||||
|
@ -433,7 +453,8 @@ export function MachineLearningDataVisualizerTableProvider(
|
|||
public async assertKeywordFieldContents(
|
||||
fieldName: string,
|
||||
docCountFormatted: string,
|
||||
topValuesCount: number
|
||||
topValuesCount: number,
|
||||
exampleContent?: string[]
|
||||
) {
|
||||
await this.assertRowExists(fieldName);
|
||||
await this.assertFieldDocCount(fieldName, docCountFormatted);
|
||||
|
@ -442,7 +463,11 @@ export function MachineLearningDataVisualizerTableProvider(
|
|||
await testSubjects.existOrFail(
|
||||
this.detailsSelector(fieldName, 'dataVisualizerFieldDataTopValuesContent')
|
||||
);
|
||||
await this.assertTopValuesContents(fieldName, topValuesCount);
|
||||
await this.assertTopValuesCount(fieldName, topValuesCount);
|
||||
|
||||
if (exampleContent) {
|
||||
await this.assertTopValuesContent(fieldName, exampleContent);
|
||||
}
|
||||
await this.ensureDetailsClosed(fieldName);
|
||||
}
|
||||
|
||||
|
@ -508,13 +533,19 @@ export function MachineLearningDataVisualizerTableProvider(
|
|||
docCountFormatted: string,
|
||||
exampleCount: number,
|
||||
viewableInLens: boolean,
|
||||
hasActionMenu?: boolean
|
||||
hasActionMenu?: boolean,
|
||||
exampleContent?: string[]
|
||||
) {
|
||||
// Currently the data used in the data visualizer tests only contains these field types.
|
||||
if (fieldType === ML_JOB_FIELD_TYPES.DATE) {
|
||||
await this.assertDateFieldContents(fieldName, docCountFormatted);
|
||||
} else if (fieldType === ML_JOB_FIELD_TYPES.KEYWORD) {
|
||||
await this.assertKeywordFieldContents(fieldName, docCountFormatted, exampleCount);
|
||||
await this.assertKeywordFieldContents(
|
||||
fieldName,
|
||||
docCountFormatted,
|
||||
exampleCount,
|
||||
exampleContent
|
||||
);
|
||||
} else if (fieldType === ML_JOB_FIELD_TYPES.TEXT) {
|
||||
await this.assertTextFieldContents(fieldName, docCountFormatted, exampleCount);
|
||||
} else if (fieldType === ML_JOB_FIELD_TYPES.GEO_POINT) {
|
||||
|
|
|
@ -19,6 +19,11 @@ export default function ({ loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(
|
||||
require.resolve('../../../../functional/apps/ml/data_visualizer/index_data_visualizer')
|
||||
);
|
||||
loadTestFile(
|
||||
require.resolve(
|
||||
'../../../../functional/apps/ml/data_visualizer/index_data_visualizer_grid_in_discover'
|
||||
)
|
||||
);
|
||||
loadTestFile(require.resolve('./index_data_visualizer_actions_panel'));
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue