[Log Explorer] Implement Data Views tab into selector (#166938)

## 📓 Summary

Closes #166848 

This work adds a new tab to navigate Data View from the Log Explorer
selector.
In this first iteration, when the user selects a data view, we move into
discovering preselecting and loading the data view of choice.

**N.B.**: this recording is made on a setup where I have no installed
integrations, so having the no integrations panel is the expected
behaviour.


e8d1f622-86fb-4841-b4cc-4a913067d2cc

## Updated selector state machine

<img width="1492" alt="Screenshot 2023-09-22 at 12 15 44"
src="c563b765-6c6c-41e8-b8cd-769c518932c3">

## New DataViews state machine

<img width="995" alt="Screenshot 2023-09-22 at 12 39 09"
src="e4e43343-c915-42d8-8660-a2ee89f8d595">

---------

Co-authored-by: Marco Antonio Ghiani <marcoantonio.ghiani@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Marco Antonio Ghiani 2023-09-28 12:21:35 +02:00 committed by GitHub
parent f8dae67d8d
commit 3be21c9e56
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 1161 additions and 92 deletions

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { DataViewListItem } from '@kbn/data-views-plugin/common';
import { AllDatasetSelection } from './all_dataset_selection';
import { SingleDatasetSelection } from './single_dataset_selection';
import { UnresolvedDatasetSelection } from './unresolved_dataset_selection';
@ -14,6 +15,7 @@ export type DatasetSelection =
| SingleDatasetSelection
| UnresolvedDatasetSelection;
export type DatasetSelectionChange = (datasetSelection: DatasetSelection) => void;
export type DataViewSelection = (dataView: DataViewListItem) => void;
export const isDatasetSelection = (input: any): input is DatasetSelection => {
return (

View file

@ -12,8 +12,10 @@ export const INTEGRATIONS_PANEL_ID = 'dataset-selector-integrations-panel';
export const INTEGRATIONS_TAB_ID = 'dataset-selector-integrations-tab';
export const UNCATEGORIZED_PANEL_ID = 'dataset-selector-uncategorized-panel';
export const UNCATEGORIZED_TAB_ID = 'dataset-selector-uncategorized-tab';
export const DATA_VIEWS_PANEL_ID = 'dataset-selector-data-views-panel';
export const DATA_VIEWS_TAB_ID = 'dataset-selector-data-views-tab';
export const DATA_VIEW_POPOVER_CONTENT_WIDTH = 300;
export const DATA_VIEW_POPOVER_CONTENT_WIDTH = 400;
export const showAllLogsLabel = i18n.translate('xpack.logExplorer.datasetSelector.showAllLogs', {
defaultMessage: 'Show all logs',
@ -28,6 +30,14 @@ export const uncategorizedLabel = i18n.translate(
{ defaultMessage: 'Uncategorized' }
);
export const dataViewsLabel = i18n.translate('xpack.logExplorer.datasetSelector.dataViews', {
defaultMessage: 'Data Views',
});
export const openDiscoverLabel = i18n.translate('xpack.logExplorer.datasetSelector.openDiscover', {
defaultMessage: 'Opens in Discover',
});
export const sortOrdersLabel = i18n.translate('xpack.logExplorer.datasetSelector.sortOrders', {
defaultMessage: 'Sort directions',
});
@ -43,6 +53,17 @@ export const noDatasetsDescriptionLabel = i18n.translate(
}
);
export const noDataViewsLabel = i18n.translate('xpack.logExplorer.datasetSelector.noDataViews', {
defaultMessage: 'No data views found',
});
export const noDataViewsDescriptionLabel = i18n.translate(
'xpack.logExplorer.datasetSelector.noDataViewsDescription',
{
defaultMessage: 'No data views or search results found.',
}
);
export const noIntegrationsLabel = i18n.translate(
'xpack.logExplorer.datasetSelector.noIntegrations',
{ defaultMessage: 'No integrations found' }

View file

@ -11,6 +11,7 @@ import React, { useState } from 'react';
import { I18nProvider } from '@kbn/i18n-react';
import type { Meta, Story } from '@storybook/react';
import { IndexPattern } from '@kbn/io-ts-utils';
import { DataViewListItem } from '@kbn/data-views-plugin/common';
import {
AllDatasetSelection,
DatasetSelection,
@ -29,6 +30,10 @@ const meta: Meta<typeof DatasetSelector> = {
options: [null, { message: 'Failed to fetch data streams' }],
control: { type: 'radio' },
},
dataViewsError: {
options: [null, { message: 'Failed to fetch data data views' }],
control: { type: 'radio' },
},
integrationsError: {
options: [null, { message: 'Failed to fetch data integrations' }],
control: { type: 'radio' },
@ -71,20 +76,39 @@ const DatasetSelectorTemplate: Story<DatasetSelectorProps> = (args) => {
const sortedDatasets = search.sortOrder === 'asc' ? filteredDatasets : filteredDatasets.reverse();
const filteredDataViews = mockDataViews.filter((dataView) =>
dataView.name?.includes(search.name as string)
);
const sortedDataViews =
search.sortOrder === 'asc' ? filteredDataViews : filteredDataViews.reverse();
const {
datasetsError,
dataViewsError,
integrationsError,
isLoadingDataViews,
isLoadingIntegrations,
isLoadingUncategorized,
} = args;
return (
<DatasetSelector
{...args}
datasets={sortedDatasets}
datasets={Boolean(datasetsError || isLoadingUncategorized) ? [] : sortedDatasets}
dataViews={Boolean(dataViewsError || isLoadingDataViews) ? [] : sortedDataViews}
datasetSelection={datasetSelection}
integrations={sortedIntegrations}
integrations={Boolean(integrationsError || isLoadingIntegrations) ? [] : sortedIntegrations}
onDataViewsSearch={setSearch}
onDataViewsSort={setSearch}
onIntegrationsLoadMore={onIntegrationsLoadMore}
onIntegrationsSearch={setSearch}
onIntegrationsSort={setSearch}
onIntegrationsStreamsSearch={setSearch}
onIntegrationsStreamsSort={setSearch}
onSelectionChange={onSelectionChange}
onUnmanagedStreamsSearch={setSearch}
onUnmanagedStreamsSort={setSearch}
onUncategorizedSearch={setSearch}
onUncategorizedSort={setSearch}
/>
);
};
@ -92,12 +116,18 @@ const DatasetSelectorTemplate: Story<DatasetSelectorProps> = (args) => {
export const Basic = DatasetSelectorTemplate.bind({});
Basic.args = {
datasetsError: null,
dataViewsError: null,
integrationsError: null,
isLoadingDataViews: false,
isLoadingIntegrations: false,
isLoadingStreams: false,
isLoadingUncategorized: false,
isSearchingIntegrations: false,
onDataViewsReload: () => alert('Reload data views...'),
onDataViewSelection: (dataView) => alert(`Navigate to data view "${dataView.name}"`),
onDataViewsTabClick: () => console.log('Load data views...'),
onIntegrationsReload: () => alert('Reload integrations...'),
onStreamsEntryClick: () => console.log('Load uncategorized streams...'),
onUnmanagedStreamsReload: () => alert('Reloading streams...'),
onUncategorizedTabClick: () => console.log('Load uncategorized streams...'),
onUncategorizedReload: () => alert('Reloading streams...'),
};
const mockIntegrations: Integration[] = [
@ -477,3 +507,25 @@ const mockDatasets: Dataset[] = [
{ name: 'data-load-balancing-logs-*' as IndexPattern },
{ name: 'data-scaling-logs-*' as IndexPattern },
].map((dataset) => Dataset.create(dataset));
const mockDataViews: DataViewListItem[] = [
{
id: 'logs-*',
namespaces: ['default'],
title: 'logs-*',
name: 'logs-*',
},
{
id: 'metrics-*',
namespaces: ['default'],
title: 'metrics-*',
name: 'metrics-*',
},
{
id: '7258d186-6430-4b51-bb67-2603cdfb4652',
namespaces: ['default'],
title: 'synthetics-*',
typeMeta: {},
name: 'synthetics-dashboard',
},
];

View file

@ -10,6 +10,9 @@ import styled from '@emotion/styled';
import { EuiContextMenu, EuiHorizontalRule, EuiTab, EuiTabs } from '@elastic/eui';
import { useIntersectionRef } from '../../hooks/use_intersection_ref';
import {
dataViewsLabel,
DATA_VIEWS_PANEL_ID,
DATA_VIEWS_TAB_ID,
DATA_VIEW_POPOVER_CONTENT_WIDTH,
integrationsLabel,
INTEGRATIONS_PANEL_ID,
@ -25,19 +28,30 @@ import { SelectorActions } from './sub_components/selector_actions';
import { DatasetSelectorProps } from './types';
import {
buildIntegrationsTree,
createDataViewsStatusItem,
createIntegrationStatusItem,
createUncategorizedStatusItem,
} from './utils';
import { getDataViewTestSubj } from '../../utils/get_data_view_test_subj';
import { DataViewsPanelTitle } from './sub_components/data_views_panel_title';
export function DatasetSelector({
datasets,
datasetsError,
datasetSelection,
datasetsError,
dataViews,
dataViewsError,
integrations,
integrationsError,
isLoadingDataViews,
isLoadingIntegrations,
isLoadingStreams,
isLoadingUncategorized,
isSearchingIntegrations,
onDataViewSelection,
onDataViewsReload,
onDataViewsSearch,
onDataViewsSort,
onDataViewsTabClick,
onIntegrationsLoadMore,
onIntegrationsReload,
onIntegrationsSearch,
@ -45,10 +59,10 @@ export function DatasetSelector({
onIntegrationsStreamsSearch,
onIntegrationsStreamsSort,
onSelectionChange,
onStreamsEntryClick,
onUnmanagedStreamsReload,
onUnmanagedStreamsSearch,
onUnmanagedStreamsSort,
onUncategorizedReload,
onUncategorizedSearch,
onUncategorizedSort,
onUncategorizedTabClick,
}: DatasetSelectorProps) {
const {
panelId,
@ -62,21 +76,26 @@ export function DatasetSelector({
searchByName,
selectAllLogDataset,
selectDataset,
selectDataView,
sortByOrder,
switchToIntegrationsTab,
switchToUncategorizedTab,
switchToDataViewsTab,
togglePopover,
} = useDatasetSelector({
initialContext: { selection: datasetSelection },
onDataViewSelection,
onDataViewsSearch,
onDataViewsSort,
onIntegrationsLoadMore,
onIntegrationsReload,
onIntegrationsSearch,
onIntegrationsSort,
onIntegrationsStreamsSearch,
onIntegrationsStreamsSort,
onUnmanagedStreamsSearch,
onUnmanagedStreamsSort,
onUnmanagedStreamsReload,
onUncategorizedSearch,
onUncategorizedSort,
onUncategorizedReload,
onSelectionChange,
});
@ -117,8 +136,8 @@ export function DatasetSelector({
createUncategorizedStatusItem({
data: datasets,
error: datasetsError,
isLoading: isLoadingStreams,
onRetry: onUnmanagedStreamsReload,
isLoading: isLoadingUncategorized,
onRetry: onUncategorizedReload,
}),
];
}
@ -127,7 +146,26 @@ export function DatasetSelector({
name: dataset.title,
onClick: () => selectDataset(dataset),
}));
}, [datasets, datasetsError, isLoadingStreams, selectDataset, onUnmanagedStreamsReload]);
}, [datasets, datasetsError, isLoadingUncategorized, selectDataset, onUncategorizedReload]);
const dataViewsItems = useMemo(() => {
if (!dataViews || dataViews.length === 0) {
return [
createDataViewsStatusItem({
data: dataViews,
error: dataViewsError,
isLoading: isLoadingDataViews,
onRetry: onDataViewsReload,
}),
];
}
return dataViews.map((dataView) => ({
'data-test-subj': getDataViewTestSubj(dataView.title),
name: dataView.name,
onClick: () => selectDataView(dataView),
}));
}, [dataViews, dataViewsError, isLoadingDataViews, selectDataView, onDataViewsReload]);
const tabs = [
{
@ -140,11 +178,20 @@ export function DatasetSelector({
id: UNCATEGORIZED_TAB_ID,
name: uncategorizedLabel,
onClick: () => {
onStreamsEntryClick(); // Lazy-load uncategorized datasets only when accessing the Uncategorized tab
onUncategorizedTabClick(); // Lazy-load uncategorized datasets only when accessing the Uncategorized tab
switchToUncategorizedTab();
},
'data-test-subj': 'datasetSelectorUncategorizedTab',
},
{
id: DATA_VIEWS_TAB_ID,
name: dataViewsLabel,
onClick: () => {
onDataViewsTabClick(); // Lazy-load data views only when accessing the Data Views tab
switchToDataViewsTab();
},
'data-test-subj': 'datasetSelectorDataViewsTab',
},
];
const tabEntries = tabs.map((tab) => (
@ -175,7 +222,7 @@ export function DatasetSelector({
search={search}
onSearch={searchByName}
onSort={sortByOrder}
isLoading={isSearchingIntegrations || isLoadingStreams}
isLoading={isSearchingIntegrations || isLoadingUncategorized}
/>
<EuiHorizontalRule margin="none" />
{/* For a smoother user experience, we keep each tab content mount and we only show the select one
@ -215,6 +262,22 @@ export function DatasetSelector({
data-test-subj="uncategorizedContextMenu"
size="s"
/>
{/* Data views tab content */}
<ContextMenu
hidden={tabId !== DATA_VIEWS_TAB_ID}
initialPanelId={DATA_VIEWS_PANEL_ID}
panels={[
{
id: DATA_VIEWS_PANEL_ID,
title: <DataViewsPanelTitle />,
width: DATA_VIEW_POPOVER_CONTENT_WIDTH,
items: dataViewsItems,
},
]}
className="eui-yScroll"
data-test-subj="dataViewsContextMenu"
size="s"
/>
</DatasetsPopover>
);
}

View file

@ -7,7 +7,7 @@
import { actions, assign, createMachine, raise } from 'xstate';
import { AllDatasetSelection, SingleDatasetSelection } from '../../../../common/dataset_selection';
import { INTEGRATIONS_TAB_ID, UNCATEGORIZED_TAB_ID } from '../constants';
import { DATA_VIEWS_TAB_ID, INTEGRATIONS_TAB_ID, UNCATEGORIZED_TAB_ID } from '../constants';
import { defaultSearch, DEFAULT_CONTEXT } from './defaults';
import {
DatasetsSelectorContext,
@ -20,7 +20,7 @@ import {
export const createPureDatasetsSelectorStateMachine = (
initialContext: Partial<DefaultDatasetsSelectorContext> = DEFAULT_CONTEXT
) =>
/** @xstate-layout N4IgpgJg5mDOIC5QBECGAXVsztgZTABswBjdAewCcA6AB3PoDcwaTDzsIBiAFQHkA4gIAyAUQDaABgC6iUPVgBLdIvIA7OSAAeiAJwBmXdUkAWSZIAcAJgBs+gIy6A7PpsAaEAE9E+q-eoW+k5OVuY24ZJONgC+0R5omNi4BMRkVHQM5Mw0DGBqXADCwnx4EjKaCsqqGkjaen7UAKySjVYONoEmFk66Ht4IjU4m1B36Y4G6k7omVrHxGFg4+ESkFDT0TCzUufn8QmJSsrWVKuqaOgi69o3UTpZWznaSuoONfYitN5K29k6tNj1nvY5iAEotkis0utMtltrQ8lxSmICjwAPoAQWEwlRxQEeFRyHRPHRpR4hwqHCqZ1qFws3Wo+kkNkaFnsJhs90a+neCBs7OoVgeJl0NnsdmuJhBYKSy1Sawymxy8LU1EUanQYCglAw1VgPFQACNEQB1ACSPAKAAlUfxUQBVAByBSJogEfAASqaAFqiZA29EAIXJx0ppxqoAu9meDMawqszUsjRFvS8iHsUf8kRMUVCjXsrn0jSlCxlKVW6Q2WS2O1V6s12rDesN1EIilgKjUUFNda1OvUsEKlvRDoEolRAAVh6JhMH5KHquc09HbPGuq4bLoLEyLDy-v4XoY41FvvYLMXEksy1CFVWlXlaxre439QaW22O12ew3dYiCu6+FiNp8KipoOjwrrukSpp8A6+IBnwPD8AAsrOIAnAuNKIAAtIKNyDPoJgOOyhanim-SDFYIxXAY+F2H4MRxKCJaXpC8qVrCNZqo+379i+b7tmqn7cX2agDqU6LulaqIBgAmqiDrokhZRHHOShhouCBYYRXw2H4FgmAmQQGSYPJ8kY8YdFyTgWACTjAox0osXKFYwtWyoPvWIlNq+rYCZ23bCY2iIemismoh6yCiO6qHodSEbYQR+hNFY7IhD0fgSjyFiNHh3yRCKorOEE57grK5bQoqcL3lxnnPs2vkfgFtXVHg6CUGAqAALYDlaw6jhOU4zuUIZqRh8UIFG5m6bGgThJuTJvKmCB3Jmop-FZoRBAx8wXhCzkVbeVUqjVT66nxDWCU1p3qK17VdWJogSVJYUKUpMXznFdQDKeIzNPo+khLZrQ8jl5nXARG5BNMhglaWrEuZVnFfl553vpdyNhrdHXdcF7qhXJEVRe9o2fRcbROMYFiTA8jgivpVgg601DTOmfhtHSfKzA5zF7eVN4ce5J08aJqN+UJzU3W12MPciaKEsSpLE1S4ZfUm-ispMfJjJIjLTLu-3UIRALfM06a6dtTG7WV17sW594AK5qCQGCalQigAF6QC+JrmlJtqgeBAiQTw0Gwf6QbDapysaVpki3ARYRRi4KV8u4S1RgZxhCs03QuACsNOXztt3iqjvO4+buexA3viZJ1ovYpykUiTKsXDhQSG40zIhOTOV0Ty9hWNZ1AZ78035nmBe8zbrkl9QZcu1Alde4auP4+F7qRdFkdoR9rfYYKRjMqegLZmKXI7unzw3FYVMvDZOX5n8U-W2xs9HfPTuL8v1er0iogogJESEkogyQ71ivvBAVM46xlGBMW+LQyJphSkYYIhFBQpUFAZF+V55TYDlNUagShOzEERNOABaJMTYlxPieWICwEqV3i3GOjgkqrkwWMOwhE6QDxcMMdkJgJ6xlsOEHB8MaD4NWIQ4hUBSH-0AXQxW4C94xy5HHUwkQOh0QeGnfoYp+TZWzMbVcLIzzcytrg9IkiyCENQIQQgZDZZAIVqApW6lMKaXPgEfSSYuh+GcLGAeHJhgckmCYcJrgZimFiIxNQ5AIBwE0I5aeaxm7Rw8VhXSFMmR6QMuYIyi1+guGZmMKyx5HC-H0GI-a-MWBpPceNLCgwjA5NPHknWQxClplFIbUp-0hiMhSuyapRd35sA4JAepY0vqDyjDGeM0NdA00mDyXw6sgghDCBEKIIyZ6I2VFM0m2EuSrVyYZTpPIUoWBKYWQwIQWSDF+Lst++z7wAAt3yHMgQCa5m54y-EqSlEIqzXC9MLGYO4Nk77PIRodJGgUzqGi+THNcxhdJtPOcZHkgjrnhPTKeawe4+QwoOgLaqGNEWvg+e2ZFHibJotvnYXS24dYMyWkspKXRB4GXFIRHWJLalzyFijeqaN-IUv7LSxpoQbDGCjFTQRllsrxlMkMEeeTrKGE3GfAVxcP7Crqj5MV4trpqCxvdKVX0HiUR1kmEIXRbUslMnSEpAJu4cj+roXV78awLwrpQD2K8DSWouIIgUkgxQihcCyc2TgB6hDjkPQi-1mh2QMlU8xpVLESNYtMiBqjh4aKiDZXwOiB55gppNaG6aWTBAFdYsMRDBLEBDQlYUAoNxUyiDlQI3J05RCSmMQwZsDCuFsPW3N6hqB2MIK2ia1wR6xh6FGHt4MTL9ruMzX48YHhEucDE6IQA */
/** @xstate-layout N4IgpgJg5mDOIC5QBECGAXVsztgZTABswBjdAewCcA6AB3PoDcwaTDzsIBiAFQHkA4gIAyAUQDaABgC6iUPVgBLdIvIA7OSAAeiAIy6ATAGZqBgBwAWMwHYzdgwE4zD6wBoQAT0RGj16gDYzIwBWKwdJC2CDawMAX1j3NExsXAJiMio6BnJmGgYwNS4AYWE+PAkZTQVlVQ0kbUQbP0kDf0l-IwddSUkXYPcvBH99ah6jNqMzFrMDXSN4xIwsHHwiUgoaeiYWanzC-iExKVl66pV1TR0EO11qI0MLDosLXWsHWYHEUOCAmJ6u3QWBxWawLEBJZapNYZTbZXK7WgFLjlMRFHgAfQAgsJhOjSgI8OjkJieJjyjxjlUODULvUrmZ-H5rLopp0jO1rMEHP5PkMZndOT5JJ1ggy4glwUsUqt0hssts8oi1NRFGp0GAoJQMLVYDxUAAjZEAdQAkjwigAJdH8dEAVQAckUSaIBHwAEomgBaomQ1sxACFKadqec6qArgBaXT+H4GZ4RSS6YIxfwWSRuTx6OaSajWfwGVrRcIRIzixbJFZpdaZLY5HZ7FVqjVa0O6g3Gs2W618IkkzHogBqJtERsJpMDlWDSlDl0QswM1AsrTTDnuSZFFl5+nZufzxmeMSTorBEOlVZh8rrioKjfVmu16jb+uohEUsBUaigJqb99bxQtmL2gIojogACoBojCEG8ghrUs4IN0DimPmoRBP4-hOO0Zi8pytwOMEnRAtEbSzGYJ5SpW0JyrW8INqqd4tjqerPq+76ql+P6MY+yJFG6fA4t26ImvaPAum6JImnw9qEv6fA8PwACy0EgGccF0ogEYFj8yZGBY9yPCELIOLyyYLhhuirjpHSzP45EVlCso1nC9ZKrezYPmoT4vm+H4cQxHmwMioiYm6Xb+gAmui9qYgpFQnDB05qeGGl6T87SkZEYzWJEm6ZkMQKmMEgQEbYjLMnZkIytWsIKgiN70e5rbMd5bGft+-l-ng7oYhF6Lusgohuspqm0slCARrpJhRI8MRvLMSa5YMZjBNpLTptywwuL4FVnlRTm1XRnEBc1rG+e1jW1Hg6CUGAqAALaBZagHAWBEFQZOCU0mGDQIb0yEGKh4wYVMMY4YmozDJyJUGMKeY7ZRjk1VedXKg1v5MQaLVnUdoZXTd92BeUIVhZF0WxcNsGjT9wQsgEwTCpYKbWJyBgmVypjro8q5vHpDjww51WXrRrlo1xnknT57Hnej6h47dD3It16K9f1g0U4lVNXMYzTOO8XQONylis3lK0LsC+jzpMDJLvzVUXjRLn1TjGMsZLbXO7L13y4TkGiGivakuS6tffBXK3GYFncnp7LssCOGTIu4zWC09P6PmtkSqeCOCw717KgArmoJAYBqVCKAAXpAzEduaVo2sJokCOJPCSdJfoTvFKmU99kaOEhpGTIKdhdHmW7tCYJHjFM7TRrMtvntRzl59QhfF3eZeVxA1d4KateCcSpKDsOo7t8HM7qQgcYBOncYMxZRXWEYW6OD8BszEuq4uBYtjz3tSPCzeVeJcoAbyru2ImoUrS9TJnFKkGse4pSmAEKYBF0IFmyvhLcpYLDUFeMnYE7ReaMl-ojIWjsC5F2AaAre4ClYqzdANIaH0u7wPghGb+Zk8z5lmrHem-Q8rbn8NQIq612TrhjPMTOFEBb2yXijFelD16UArmAw0KI-YYgPmSUQFJmEjQQeNOMOC5hxnpq0IqrQtx6SEamF4S5SKOAsCQnOciGwQCWAORQYAADuT4a5dnriJMSEkpJjgDGfJKP1xhm3vn0bknRWRj2TMImGQQ0yhEMLoZxsiDquXcZgTxPi-E707HXHsDonSN3dF6H0p89Hd3goCO4bRIjJ2TjTFakhsICNMYuQIQ8lyzwzuWSqC99rIzcR4rxvjt7BUgcrUmMVYFThDhfYYOZ3hWEyT0FanIsGODuFMecycZhRDLJKeydtF65JvPk1AhSZm0LdD1SKqsmGd30fBdZdxHiBANsCTk7wtxzFuC8NBBskwuEcNk65Ey8lTKKbM1Emi+xHxHBEzWiAR4cycARLky0IiLT0HGBcekZiWFCOyGYMLMjYFlLUagShPzECCsirEAl8SEi0UHeprCL5RlXKMdk5hOhf0mO0LczMmR8OBPhdkjwnFSMuWMmgdL1gMqZVAFl6j-bcp0RigxEYohIQjoEbojgfDhGNoMaM4RcEvALKmEIQQug0tVVRBlqBCCEFZRogO2jdEfIafy6MPx0Lj1TEEdo3Itypj8M4VMDNHjT1BGCNQ5AIBwE0FnGRGw4GrLGhGbhEMMr01hpEeO1BVwhF8HmRMI9ghurIZQfN59C3JiQulFkmVy38JtcMRONabB6RaM8PmSrRl-2bdQNgHBICtsiVcNOJhojpheJZHwHw8pLhweMXwqZzB6Qtk23OKMF2YvGgRW4XaKVZQrXlOMZgq0+AIlC0UpkT2uNcgACx8uegxjIn1OABq8V42CYi8h8EI6OoR0xdL+eOkZu1SGnsOh1F2-62FWBMDentvgcq8heE+543RcLjEkFEVNSHs45LhU7dDj5mq-vfJhi+zNcE03Se0IqcwgS8neBPRMsMxHpjhhO5DLibmow9uLTGp0pYyfgCsttP1NLtFGN0ZwYLlrLQBryRkxjMq2FFYzLJ4maOwoAdJhjsnXatT8hdT2+MHqsbGo4BcwouQxCsJ50U+m7DPsZDGOt9NOifqkwotepdlGb2Yq51T5ghERzZAe5MGFME9NpuYA2HR8KwcfuFujyo7kPKfPFq4pZbjDCBCyZaPn8zPyTFWwE+EpjW2FE2tVZBImfP5cmeN0YI4tGrVarcIi7jxJBN-EFVGLmTtIV10MjL2LEHKxpXSnbIif3uBhaI0YtwgwCDDXw+hy3QvM7m2lHr1DUC9YQNbCEmu+GHZ-Dh+g+16Ajjg47zJjDLU6ZI+IQA */
createMachine<DatasetsSelectorContext, DatasetsSelectorEvent, DatasetsSelectorTypestate>(
{
context: { ...DEFAULT_CONTEXT, ...initialContext },
@ -55,6 +55,7 @@ export const createPureDatasetsSelectorStateMachine = (
entry: ['storeIntegrationsTabId'],
on: {
SWITCH_TO_UNCATEGORIZED_TAB: 'uncategorizedTab',
SWITCH_TO_DATA_VIEWS_TAB: 'dataViewsTab',
},
states: {
hist: {
@ -106,15 +107,37 @@ export const createPureDatasetsSelectorStateMachine = (
],
on: {
SWITCH_TO_INTEGRATIONS_TAB: 'integrationsTab.hist',
SWITCH_TO_DATA_VIEWS_TAB: 'dataViewsTab',
SEARCH_BY_NAME: {
actions: ['storeSearch', 'searchUnmanagedStreams'],
actions: ['storeSearch', 'searchUncategorized'],
},
SORT_BY_ORDER: {
actions: ['storeSearch', 'sortUnmanagedStreams'],
actions: ['storeSearch', 'sortUncategorized'],
},
SELECT_DATASET: '#closed',
},
},
dataViewsTab: {
entry: [
'storeDataViewsTabId',
'retrieveSearchFromCache',
'maybeRestoreSearchResult',
],
on: {
SWITCH_TO_INTEGRATIONS_TAB: 'integrationsTab.hist',
SWITCH_TO_UNCATEGORIZED_TAB: 'uncategorizedTab',
SEARCH_BY_NAME: {
actions: ['storeSearch', 'searchDataViews'],
},
SORT_BY_ORDER: {
actions: ['storeSearch', 'sortDataViews'],
},
SELECT_DATA_VIEW: {
target: '#closed',
actions: ['selectDataView'],
},
},
},
},
},
},
@ -149,12 +172,13 @@ export const createPureDatasetsSelectorStateMachine = (
actions: {
storeIntegrationsTabId: assign((_context) => ({ tabId: INTEGRATIONS_TAB_ID })),
storeUncategorizedTabId: assign((_context) => ({ tabId: UNCATEGORIZED_TAB_ID })),
storeDataViewsTabId: assign((_context) => ({ tabId: DATA_VIEWS_TAB_ID })),
storePanelId: assign((_context, event) =>
'panelId' in event ? { panelId: event.panelId } : {}
),
storeSearch: assign((context, event) => {
if ('search' in event) {
const id = context.tabId === UNCATEGORIZED_TAB_ID ? context.tabId : context.panelId;
const id = context.tabId === INTEGRATIONS_TAB_ID ? context.panelId : context.tabId;
context.searchCache.set(id, event.search);
return {
@ -179,6 +203,9 @@ export const createPureDatasetsSelectorStateMachine = (
if (event.type === 'SWITCH_TO_UNCATEGORIZED_TAB' && 'tabId' in context) {
return { search: context.searchCache.get(context.tabId) ?? defaultSearch };
}
if (event.type === 'SWITCH_TO_DATA_VIEWS_TAB' && 'tabId' in context) {
return { search: context.searchCache.get(context.tabId) ?? defaultSearch };
}
return {};
}),
maybeRestoreSearchResult: actions.pure((context, event) => {
@ -188,8 +215,15 @@ export const createPureDatasetsSelectorStateMachine = (
event.type === 'SWITCH_TO_INTEGRATIONS_TAB' && context.searchCache.has(context.panelId);
const hasSearchOnUncategorizedTab =
event.type === 'SWITCH_TO_UNCATEGORIZED_TAB' && context.searchCache.has(context.tabId);
const hasSearchOnDataViewsTab =
event.type === 'SWITCH_TO_DATA_VIEWS_TAB' && context.searchCache.has(context.tabId);
if (hasSearchOnChangePanel || hasSearchOnIntegrationsTab || hasSearchOnUncategorizedTab) {
if (
hasSearchOnChangePanel ||
hasSearchOnIntegrationsTab ||
hasSearchOnUncategorizedTab ||
hasSearchOnDataViewsTab
) {
return raise({ type: 'SORT_BY_ORDER', search: context.search });
}
}),
@ -199,16 +233,19 @@ export const createPureDatasetsSelectorStateMachine = (
export const createDatasetsSelectorStateMachine = ({
initialContext,
onDataViewSelection,
onDataViewsSearch,
onDataViewsSort,
onIntegrationsLoadMore,
onIntegrationsReload,
onIntegrationsSearch,
onIntegrationsSort,
onIntegrationsStreamsSearch,
onIntegrationsStreamsSort,
onUnmanagedStreamsSearch,
onUnmanagedStreamsSort,
onUncategorizedSearch,
onUncategorizedSort,
onSelectionChange,
onUnmanagedStreamsReload,
onUncategorizedReload,
}: DatasetsSelectorStateMachineDependencies) =>
createPureDatasetsSelectorStateMachine(initialContext).withConfig({
actions: {
@ -217,7 +254,12 @@ export const createDatasetsSelectorStateMachine = ({
},
loadMoreIntegrations: onIntegrationsLoadMore,
relaodIntegrations: onIntegrationsReload,
reloadUnmanagedStreams: onUnmanagedStreamsReload,
reloadUncategorized: onUncategorizedReload,
selectDataView: (_context, event) => {
if (event.type === 'SELECT_DATA_VIEW' && 'dataView' in event) {
return onDataViewSelection(event.dataView);
}
},
// Search actions
searchIntegrations: (_context, event) => {
if ('search' in event) {
@ -229,6 +271,16 @@ export const createDatasetsSelectorStateMachine = ({
onIntegrationsSort(event.search);
}
},
searchDataViews: (context, event) => {
if ('search' in event) {
onDataViewsSearch(event.search);
}
},
sortDataViews: (context, event) => {
if ('search' in event) {
onDataViewsSort(event.search);
}
},
searchIntegrationsStreams: (context, event) => {
if ('search' in event) {
onIntegrationsStreamsSearch({ ...event.search, integrationId: context.panelId });
@ -239,14 +291,14 @@ export const createDatasetsSelectorStateMachine = ({
onIntegrationsStreamsSort({ ...event.search, integrationId: context.panelId });
}
},
searchUnmanagedStreams: (_context, event) => {
searchUncategorized: (_context, event) => {
if ('search' in event) {
onUnmanagedStreamsSearch(event.search);
onUncategorizedSearch(event.search);
}
},
sortUnmanagedStreams: (_context, event) => {
sortUncategorized: (_context, event) => {
if ('search' in event) {
onUnmanagedStreamsSort(event.search);
onUncategorizedSort(event.search);
}
},
},

View file

@ -4,7 +4,13 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { DatasetSelection, DatasetSelectionChange } from '../../../../common/dataset_selection';
import { DataViewListItem } from '@kbn/data-views-plugin/common';
import { SearchDataViews } from '../../../hooks/use_data_views';
import {
DatasetSelection,
DatasetSelectionChange,
DataViewSelection,
} from '../../../../common/dataset_selection';
import { Dataset } from '../../../../common/datasets/models/dataset';
import { ReloadDatasets, SearchDatasets } from '../../../hooks/use_datasets';
import {
@ -56,6 +62,10 @@ export type DatasetsSelectorTypestate =
value: 'popover.open.uncategorizedTab';
context: DefaultDatasetsSelectorContext;
}
| {
value: 'popover.open.dataViewsTab';
context: DefaultDatasetsSelectorContext;
}
| {
value: 'selection';
context: DefaultDatasetsSelectorContext;
@ -84,6 +94,9 @@ export type DatasetsSelectorEvent =
| {
type: 'SWITCH_TO_UNCATEGORIZED_TAB';
}
| {
type: 'SWITCH_TO_DATA_VIEWS_TAB';
}
| {
type: 'CHANGE_PANEL';
panelId: PanelId;
@ -92,6 +105,10 @@ export type DatasetsSelectorEvent =
type: 'SELECT_DATASET';
dataset: Dataset;
}
| {
type: 'SELECT_DATA_VIEW';
dataView: DataViewListItem;
}
| {
type: 'SELECT_ALL_LOGS_DATASET';
}
@ -109,6 +126,9 @@ export type DatasetsSelectorEvent =
export interface DatasetsSelectorStateMachineDependencies {
initialContext?: Partial<DefaultDatasetsSelectorContext>;
onDataViewSelection: DataViewSelection;
onDataViewsSearch: SearchDataViews;
onDataViewsSort: SearchDataViews;
onIntegrationsLoadMore: LoadMoreIntegrations;
onIntegrationsReload: ReloadIntegrations;
onIntegrationsSearch: SearchIntegrations;
@ -116,7 +136,7 @@ export interface DatasetsSelectorStateMachineDependencies {
onIntegrationsStreamsSearch: SearchIntegrations;
onIntegrationsStreamsSort: SearchIntegrations;
onSelectionChange: DatasetSelectionChange;
onUnmanagedStreamsReload: ReloadDatasets;
onUnmanagedStreamsSearch: SearchDatasets;
onUnmanagedStreamsSort: SearchDatasets;
onUncategorizedReload: ReloadDatasets;
onUncategorizedSearch: SearchDatasets;
onUncategorizedSort: SearchDatasets;
}

View file

@ -11,6 +11,7 @@ import {
ChangePanelHandler,
DatasetSelectionHandler,
DatasetsSelectorSearchHandler,
DataViewSelectionHandler,
PanelId,
} from '../types';
import { createDatasetsSelectorStateMachine } from './state_machine';
@ -18,6 +19,9 @@ import { DatasetsSelectorStateMachineDependencies } from './types';
export const useDatasetSelector = ({
initialContext,
onDataViewSelection,
onDataViewsSearch,
onDataViewsSort,
onIntegrationsLoadMore,
onIntegrationsReload,
onIntegrationsSearch,
@ -25,13 +29,16 @@ export const useDatasetSelector = ({
onIntegrationsStreamsSearch,
onIntegrationsStreamsSort,
onSelectionChange,
onUnmanagedStreamsSearch,
onUnmanagedStreamsSort,
onUnmanagedStreamsReload,
onUncategorizedSearch,
onUncategorizedSort,
onUncategorizedReload,
}: DatasetsSelectorStateMachineDependencies) => {
const datasetsSelectorStateService = useInterpret(() =>
createDatasetsSelectorStateMachine({
initialContext,
onDataViewSelection,
onDataViewsSearch,
onDataViewsSort,
onIntegrationsLoadMore,
onIntegrationsReload,
onIntegrationsSearch,
@ -39,9 +46,9 @@ export const useDatasetSelector = ({
onIntegrationsStreamsSearch,
onIntegrationsStreamsSort,
onSelectionChange,
onUnmanagedStreamsSearch,
onUnmanagedStreamsSort,
onUnmanagedStreamsReload,
onUncategorizedSearch,
onUncategorizedSort,
onUncategorizedReload,
})
);
@ -64,6 +71,11 @@ export const useDatasetSelector = ({
[datasetsSelectorStateService]
);
const switchToDataViewsTab = useCallback(
() => datasetsSelectorStateService.send({ type: 'SWITCH_TO_DATA_VIEWS_TAB' }),
[datasetsSelectorStateService]
);
const changePanel = useCallback<ChangePanelHandler>(
(panelDetails) =>
datasetsSelectorStateService.send({
@ -93,6 +105,11 @@ export const useDatasetSelector = ({
[datasetsSelectorStateService]
);
const selectDataView = useCallback<DataViewSelectionHandler>(
(dataView) => datasetsSelectorStateService.send({ type: 'SELECT_DATA_VIEW', dataView }),
[datasetsSelectorStateService]
);
const sortByOrder = useCallback<DatasetsSelectorSearchHandler>(
(params) => datasetsSelectorStateService.send({ type: 'SORT_BY_ORDER', search: params }),
[datasetsSelectorStateService]
@ -124,9 +141,11 @@ export const useDatasetSelector = ({
searchByName,
selectAllLogDataset,
selectDataset,
selectDataView,
sortByOrder,
switchToIntegrationsTab,
switchToUncategorizedTab,
switchToDataViewsTab,
togglePopover,
};
};

View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { dataViewsLabel, openDiscoverLabel } from '../constants';
export const DataViewsPanelTitle = () => {
return (
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem>{dataViewsLabel}</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBadge iconType="discoverApp">{openDiscoverLabel}</EuiBadge>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -8,13 +8,14 @@
import React from 'react';
import { EuiButton, EuiEmptyPrompt, EuiText, EuiToolTip } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import type { DataViewListItem } from '@kbn/data-views-plugin/common';
import { ReloadDatasets } from '../../../hooks/use_datasets';
import { errorLabel, noDataRetryLabel } from '../constants';
import type { Dataset, Integration } from '../../../../common/datasets';
import { DatasetSkeleton } from './datasets_skeleton';
export interface ListStatusProps {
data: Dataset[] | Integration[] | null;
data: Dataset[] | Integration[] | DataViewListItem[] | null;
description: string;
error: Error | null;
isLoading: boolean;

View file

@ -6,7 +6,12 @@
*/
import { EuiContextMenuPanelId } from '@elastic/eui/src/components/context_menu/context_menu';
import type { DatasetSelection, DatasetSelectionChange } from '../../../common/dataset_selection';
import { DataViewListItem } from '@kbn/data-views-plugin/common';
import type {
DatasetSelection,
DatasetSelectionChange,
DataViewSelection,
} from '../../../common/dataset_selection';
import { SortOrder } from '../../../common/latest';
import { Dataset, Integration, IntegrationId } from '../../../common/datasets';
import { LoadDatasets, ReloadDatasets, SearchDatasets } from '../../hooks/use_datasets';
@ -15,7 +20,13 @@ import {
ReloadIntegrations,
SearchIntegrations,
} from '../../hooks/use_integrations';
import { INTEGRATIONS_PANEL_ID, INTEGRATIONS_TAB_ID, UNCATEGORIZED_TAB_ID } from './constants';
import {
DATA_VIEWS_TAB_ID,
INTEGRATIONS_PANEL_ID,
INTEGRATIONS_TAB_ID,
UNCATEGORIZED_TAB_ID,
} from './constants';
import { LoadDataViews, ReloadDataViews, SearchDataViews } from '../../hooks/use_data_views';
export interface DatasetSelectorProps {
/* The generic data stream list */
@ -24,36 +35,52 @@ export interface DatasetSelectorProps {
datasetsError: Error | null;
/* The current selection instance */
datasetSelection: DatasetSelection;
/* The available data views list */
dataViews: DataViewListItem[] | null;
/* Any error occurred to show when the user preview the data views */
dataViewsError: Error | null;
/* The integrations list, each integration includes its data streams */
integrations: Integration[] | null;
/* Any error occurred to show when the user preview the integrations */
integrationsError: Error | null;
/* Flags for loading/searching integrations or data streams*/
/* Flags for loading/searching integrations, data streams or data views*/
isLoadingDataViews: boolean;
isLoadingIntegrations: boolean;
isLoadingStreams: boolean;
isLoadingUncategorized: boolean;
isSearchingIntegrations: boolean;
/* Triggered when retrying to load the data views */
onDataViewsReload: ReloadDataViews;
/* Triggered when selecting a data view */
onDataViewSelection: DataViewSelection;
/* Triggered when the data views tab is selected */
onDataViewsTabClick: LoadDataViews;
/* Triggered when we reach the bottom of the integration list and want to load more */
onIntegrationsLoadMore: LoadMoreIntegrations;
/* Triggered when the user reload the list after an error */
onIntegrationsReload: ReloadIntegrations;
/* Triggered when a search or sorting is performed */
onDataViewsSearch: SearchDataViews;
onDataViewsSort: SearchDataViews;
onIntegrationsSearch: SearchIntegrations;
onIntegrationsSort: SearchIntegrations;
onIntegrationsStreamsSearch: SearchIntegrations;
onIntegrationsStreamsSort: SearchIntegrations;
onUnmanagedStreamsSearch: SearchDatasets;
onUnmanagedStreamsSort: SearchDatasets;
onUncategorizedSearch: SearchDatasets;
onUncategorizedSort: SearchDatasets;
/* Triggered when retrying to load the data streams */
onUnmanagedStreamsReload: ReloadDatasets;
/* Triggered when the uncategorized streams entry is selected */
onStreamsEntryClick: LoadDatasets;
onUncategorizedReload: ReloadDatasets;
/* Triggered when the uncategorized tab is selected */
onUncategorizedTabClick: LoadDatasets;
/* Triggered when the selection is updated */
onSelectionChange: DatasetSelectionChange;
}
export type PanelId = typeof INTEGRATIONS_PANEL_ID | IntegrationId;
export type TabId = typeof INTEGRATIONS_TAB_ID | typeof UNCATEGORIZED_TAB_ID;
export type TabId =
| typeof INTEGRATIONS_TAB_ID
| typeof UNCATEGORIZED_TAB_ID
| typeof DATA_VIEWS_TAB_ID;
export interface SearchParams {
integrationId?: PanelId;
@ -68,3 +95,5 @@ export type DatasetsSelectorSearchHandler = (params: DatasetsSelectorSearchParam
export type ChangePanelHandler = ({ panelId }: { panelId: EuiContextMenuPanelId }) => void;
export type DatasetSelectionHandler = (dataset: Dataset) => void;
export type DataViewSelectionHandler = (dataView: DataViewListItem) => void;

View file

@ -13,6 +13,8 @@ import {
DATA_VIEW_POPOVER_CONTENT_WIDTH,
noDatasetsDescriptionLabel,
noDatasetsLabel,
noDataViewsDescriptionLabel,
noDataViewsLabel,
noIntegrationsDescriptionLabel,
noIntegrationsLabel,
} from './constants';
@ -116,3 +118,19 @@ export const createUncategorizedStatusItem = (
),
};
};
export const createDataViewsStatusItem = (
props: Omit<ListStatusProps, 'description' | 'title'>
) => {
return {
disabled: true,
name: (
<ListStatus
key="dataViewsStatusItem"
description={noDataViewsDescriptionLabel}
title={noDataViewsLabel}
{...props}
/>
),
};
};

View file

@ -8,19 +8,19 @@
import React, { useMemo } from 'react';
import { ScopedHistory } from '@kbn/core-application-browser';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { DiscoverAppState, DiscoverStart } from '@kbn/discover-plugin/public';
import { DiscoverAppState } from '@kbn/discover-plugin/public';
import type { BehaviorSubject } from 'rxjs';
import { CoreStart } from '@kbn/core/public';
import { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
import { HIDE_ANNOUNCEMENTS } from '@kbn/discover-utils';
import {
createLogExplorerProfileCustomizations,
CreateLogExplorerProfileCustomizationsDeps,
} from '../../customizations/log_explorer_profile';
import { createLogExplorerProfileCustomizations } from '../../customizations/log_explorer_profile';
import { createPropertyGetProxy } from '../../utils/proxies';
import { LogExplorerProfileContext } from '../../state_machines/log_explorer_profile';
import { LogExplorerStartDeps } from '../../types';
export interface CreateLogExplorerArgs extends CreateLogExplorerProfileCustomizationsDeps {
discover: DiscoverStart;
export interface CreateLogExplorerArgs {
core: CoreStart;
plugins: LogExplorerStartDeps;
}
export interface LogExplorerStateContainer {
@ -33,11 +33,12 @@ export interface LogExplorerProps {
state$?: BehaviorSubject<LogExplorerStateContainer>;
}
export const createLogExplorer = ({
core,
data,
discover: { DiscoverContainer },
}: CreateLogExplorerArgs) => {
export const createLogExplorer = ({ core, plugins }: CreateLogExplorerArgs) => {
const {
data,
discover: { DiscoverContainer },
} = plugins;
const overrideServices = {
data: createDataServiceProxy(data),
uiSettings: createUiSettingsServiceProxy(core.uiSettings),
@ -45,7 +46,7 @@ export const createLogExplorer = ({
return ({ scopedHistory, state$ }: LogExplorerProps) => {
const logExplorerCustomizations = useMemo(
() => [createLogExplorerProfileCustomizations({ core, data, state$ })],
() => [createLogExplorerProfileCustomizations({ core, plugins, state$ })],
[state$]
);

View file

@ -6,12 +6,15 @@
*/
import React from 'react';
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { DiscoverStart } from '@kbn/discover-plugin/public';
import { DatasetSelector } from '../components/dataset_selector';
import { DatasetsProvider, useDatasetsContext } from '../hooks/use_datasets';
import { IntegrationsProvider, useIntegrationsContext } from '../hooks/use_integrations';
import { IDatasetsClient } from '../services/datasets';
import { LogExplorerProfileStateService } from '../state_machines/log_explorer_profile';
import { useDatasetSelection } from '../hooks/use_dataset_selection';
import { DataViewsProvider, useDataViewsContext } from '../hooks/use_data_views';
interface CustomDatasetSelectorProps {
logExplorerProfileStateService: LogExplorerProfileStateService;
@ -38,23 +41,42 @@ export const CustomDatasetSelector = withProviders(({ logExplorerProfileStateSer
const {
datasets,
error: datasetsError,
isLoading: isLoadingStreams,
isLoading: isLoadingUncategorized,
loadDatasets,
reloadDatasets,
searchDatasets,
sortDatasets,
} = useDatasetsContext();
const {
dataViews,
error: dataViewsError,
isLoading: isLoadingDataViews,
loadDataViews,
reloadDataViews,
selectDataView,
searchDataViews,
sortDataViews,
} = useDataViewsContext();
return (
<DatasetSelector
datasets={datasets}
datasetSelection={datasetSelection}
datasetsError={datasetsError}
dataViews={dataViews}
dataViewsError={dataViewsError}
integrations={integrations}
integrationsError={integrationsError}
isLoadingDataViews={isLoadingDataViews}
isLoadingIntegrations={isLoadingIntegrations}
isLoadingStreams={isLoadingStreams}
isLoadingUncategorized={isLoadingUncategorized}
isSearchingIntegrations={isSearchingIntegrations}
onDataViewsReload={reloadDataViews}
onDataViewsSearch={searchDataViews}
onDataViewSelection={selectDataView}
onDataViewsSort={sortDataViews}
onDataViewsTabClick={loadDataViews}
onIntegrationsLoadMore={loadMore}
onIntegrationsReload={reloadIntegrations}
onIntegrationsSearch={searchIntegrations}
@ -62,10 +84,10 @@ export const CustomDatasetSelector = withProviders(({ logExplorerProfileStateSer
onIntegrationsStreamsSearch={searchIntegrationsStreams}
onIntegrationsStreamsSort={sortIntegrationsStreams}
onSelectionChange={handleDatasetSelectionChange}
onStreamsEntryClick={loadDatasets}
onUnmanagedStreamsReload={reloadDatasets}
onUnmanagedStreamsSearch={searchDatasets}
onUnmanagedStreamsSort={sortDatasets}
onUncategorizedReload={reloadDatasets}
onUncategorizedSearch={searchDatasets}
onUncategorizedSort={sortDatasets}
onUncategorizedTabClick={loadDatasets}
/>
);
});
@ -75,17 +97,23 @@ export default CustomDatasetSelector;
export type CustomDatasetSelectorBuilderProps = CustomDatasetSelectorProps & {
datasetsClient: IDatasetsClient;
dataViews: DataViewsPublicPluginStart;
discover: DiscoverStart;
};
function withProviders(Component: React.FunctionComponent<CustomDatasetSelectorProps>) {
return function ComponentWithProviders({
logExplorerProfileStateService,
datasetsClient,
dataViews,
discover,
logExplorerProfileStateService,
}: CustomDatasetSelectorBuilderProps) {
return (
<IntegrationsProvider datasetsClient={datasetsClient}>
<DatasetsProvider datasetsClient={datasetsClient}>
<Component logExplorerProfileStateService={logExplorerProfileStateService} />
<DataViewsProvider dataViewsService={dataViews} discoverService={discover}>
<Component logExplorerProfileStateService={logExplorerProfileStateService} />
</DataViewsProvider>
</DatasetsProvider>
</IntegrationsProvider>
);

View file

@ -4,8 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { CoreStart } from '@kbn/core/public';
import { CustomizationCallback, DiscoverStateContainer } from '@kbn/discover-plugin/public';
import React from 'react';
@ -13,19 +11,21 @@ import { type BehaviorSubject, combineLatest, from, map, Subscription } from 'rx
import { dynamic } from '../utils/dynamic';
import { LogExplorerProfileStateService } from '../state_machines/log_explorer_profile';
import { LogExplorerStateContainer } from '../components/log_explorer';
import { LogExplorerStartDeps } from '../types';
const LazyCustomDatasetSelector = dynamic(() => import('./custom_dataset_selector'));
const LazyCustomDatasetFilters = dynamic(() => import('./custom_dataset_filters'));
export interface CreateLogExplorerProfileCustomizationsDeps {
core: CoreStart;
data: DataPublicPluginStart;
plugins: LogExplorerStartDeps;
state$?: BehaviorSubject<LogExplorerStateContainer>;
}
export const createLogExplorerProfileCustomizations =
({ core, data, state$ }: CreateLogExplorerProfileCustomizationsDeps): CustomizationCallback =>
({ core, plugins, state$ }: CreateLogExplorerProfileCustomizationsDeps): CustomizationCallback =>
async ({ customizations, stateContainer }) => {
const { data, dataViews, discover } = plugins;
// Lazy load dependencies
const datasetServiceModuleLoadable = import('../services/datasets');
const logExplorerMachineModuleLoadable = import('../state_machines/log_explorer_profile');
@ -72,6 +72,8 @@ export const createLogExplorerProfileCustomizations =
CustomDataViewPicker: () => (
<LazyCustomDatasetSelector
datasetsClient={datasetsClient}
dataViews={dataViews}
discover={discover}
logExplorerProfileStateService={logExplorerProfileStateService}
/>
),

View file

@ -0,0 +1,104 @@
/*
* 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 { useCallback } from 'react';
import createContainer from 'constate';
import { useInterpret, useSelector } from '@xstate/react';
import { DataViewListItem, DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { DiscoverStart } from '@kbn/discover-plugin/public';
import { SortOrder } from '../../common/latest';
import { createDataViewsStateMachine } from '../state_machines/data_views';
interface DataViewsContextDeps {
dataViewsService: DataViewsPublicPluginStart;
discoverService: DiscoverStart;
}
export interface SearchDataViewsParams {
name: string;
sortOrder: SortOrder;
}
export type DataViewSelectionHandler = (dataView: DataViewListItem) => void;
export type SearchDataViews = (params: SearchDataViewsParams) => void;
export type LoadDataViews = () => void;
export type ReloadDataViews = () => void;
const useDataViews = ({ dataViewsService, discoverService }: DataViewsContextDeps) => {
const dataViewsStateService = useInterpret(() =>
createDataViewsStateMachine({
dataViews: dataViewsService,
discover: discoverService,
})
);
const dataViews = useSelector(dataViewsStateService, (state) => state.context.dataViews);
const error = useSelector(dataViewsStateService, (state) => state.context.error);
const isLoading = useSelector(dataViewsStateService, (state) => state.matches('loading'));
const loadDataViews = useCallback(
() => dataViewsStateService.send({ type: 'LOAD_DATA_VIEWS' }),
[dataViewsStateService]
);
const reloadDataViews = useCallback(
() => dataViewsStateService.send({ type: 'RELOAD_DATA_VIEWS' }),
[dataViewsStateService]
);
const searchDataViews: SearchDataViews = useCallback(
(searchParams) =>
dataViewsStateService.send({
type: 'SEARCH_DATA_VIEWS',
search: searchParams,
}),
[dataViewsStateService]
);
const selectDataView: DataViewSelectionHandler = useCallback(
(dataView) =>
dataViewsStateService.send({
type: 'SELECT_DATA_VIEW',
dataView,
}),
[dataViewsStateService]
);
const sortDataViews: SearchDataViews = useCallback(
(searchParams) =>
dataViewsStateService.send({
type: 'SORT_DATA_VIEWS',
search: searchParams,
}),
[dataViewsStateService]
);
return {
// Underlying state machine
dataViewsStateService,
// Failure states
error,
// Loading states
isLoading,
// Data
dataViews,
// Actions
loadDataViews,
reloadDataViews,
searchDataViews,
selectDataView,
sortDataViews,
};
};
export const [DataViewsProvider, useDataViewsContext] = createContainer(useDataViews);

View file

@ -40,12 +40,9 @@ export class LogExplorerPlugin implements Plugin<LogExplorerPluginSetup, LogExpl
}
public start(core: CoreStart, plugins: LogExplorerStartDeps) {
const { data, discover } = plugins;
const LogExplorer = createLogExplorer({
core,
data,
discover,
plugins,
});
return {

View file

@ -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 * from './src';

View file

@ -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 { HashedCache } from '../../../../common/hashed_cache';
import { DefaultDataViewsContext } from './types';
export const DEFAULT_CONTEXT: DefaultDataViewsContext = {
cache: new HashedCache(),
dataViewsSource: null,
dataViews: null,
error: null,
search: {
name: '',
sortOrder: 'asc',
},
};

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export * from './state_machine';
export * from './types';

View file

@ -0,0 +1,160 @@
/*
* 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 { DataViewListItem, DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { isError } from 'lodash';
import { assign, createMachine } from 'xstate';
import { DiscoverStart } from '@kbn/discover-plugin/public';
import { parseDataViewListItem } from '../../../utils/parse_data_view_list_item';
import { createComparatorByField } from '../../../utils/comparator_by_field';
import { DEFAULT_CONTEXT } from './defaults';
import type {
DataViewsContext,
DataViewsEvent,
DataViewsSearchParams,
DataViewsTypestate,
DefaultDataViewsContext,
} from './types';
export const createPureDataViewsStateMachine = (
initialContext: DefaultDataViewsContext = DEFAULT_CONTEXT
) =>
createMachine<DataViewsContext, DataViewsEvent, DataViewsTypestate>(
{
/** @xstate-layout N4IgpgJg5mDOIC5QBECGAXVA1AlmA7rAHQCuAdjhejqgDY4BekAxADIDyAgsgPrKcAVTjywBJAKIB1AMoBtAAwBdRKAAOAe1g5q6sipAAPRABYAzKaIB2a6bOWArABoQAT0QBaAJzyi9y8YAmAEYADgcAX3DnNExcAmJadVQISihmCF0wIkoAN3UAayyY7DxCIkTk1IRc9QBjDBxdBUVm-Q0tHT0kQxNjSyJPP3lTIICnV0RLTyJ5eUsQ0ftI6IwS+PKklLI0sAAnXfVdolVaDAAzQ4BbImK4soqtqGqyPPrO5tbu9u1GrtAjBAANgcRFMDmcbgQIPsIQCw0WyxAt1KCU2kGyEFoYGY0nEnAASgBhAASfEEwjEUjkSjamh+un0AOsgN8nksgPsgLGEJMHKIgNCgRC9mMC2siOR6we6JwmOx0nY+IEZKEIgkMk+ajpnUZJk8ISIAQCHK540hpgCBoWgOM8lNEtWd1RyRlcpx4lY4kJyv4qspkk1IG+Ou6AMB8gNfkCoXBEwQ9gtM1GgzZ+qC4qiSMdKI2LogRAgYAARupyLUwNIwKhdrUABapSWEd0Ekkqinq6nKL7a366hBBeT2ez8zncuMJ6ZzEX2VNiywO2I56X5wslssVqs1+vbRuwZgGWCYdBZVBnY+7AAUplmAEpmLvc4WV8XS2Ry5Xq3WG9n4oHg73Q0mEIDXDYwwLMexLUBMEeQQcwLE8II0wCYxARCcMpwXNZ7k2VIADFUBwLEIGYfEPS4XhfXbKk-x7BlAKBaDfACSxbDBM0PACUFPGMBMEURMh1ELeBul3WkOgA-5EFMYUrBsOwOIQdwUK4oYRjGLCnVICgqBoegmAgcT6T+HoEDAlkxnmRZYPcIIhxmSx4Q0zMHweVIjJDKSzNtIgQlmKyx0hZThxCHi+OclZFylNFDO7CT6K83ifCNE1AsQFCglBIIbTtCKsyinC8wxLEPMk0zARtUF7Ds01YN44dAj8OFglFBTNKXGKCxfdcPy3b8CpErV4pMgEBztUEVJjRSzEyzxLOaoJWvY9rosqbYCKIyBSoS0y5q4nLarjZT+j8BZnMiIA */
context: initialContext,
preserveActionOrder: true,
predictableActionArguments: true,
id: 'DataViews',
initial: 'uninitialized',
states: {
uninitialized: {
on: {
LOAD_DATA_VIEWS: 'loading',
},
},
loading: {
id: 'loading',
invoke: {
src: 'loadDataViews',
onDone: {
target: 'loaded',
actions: ['storeInCache', 'storeDataViews', 'storeSearch'],
},
onError: 'loadingFailed',
},
},
loaded: {
id: 'loaded',
initial: 'idle',
states: {
idle: {
on: {
SEARCH_DATA_VIEWS: 'debounceSearchingDataViews',
SORT_DATA_VIEWS: {
actions: ['storeSearch', 'searchDataViews'],
},
SELECT_DATA_VIEW: {
actions: ['navigateToDiscoverDataView'],
},
},
},
debounceSearchingDataViews: {
entry: 'storeSearch',
on: {
SEARCH_DATA_VIEWS: 'debounceSearchingDataViews',
},
after: {
300: {
target: 'idle',
actions: 'searchDataViews',
},
},
},
},
},
loadingFailed: {
entry: ['clearCache', 'clearData', 'storeError'],
exit: 'clearError',
on: {
RELOAD_DATA_VIEWS: 'loading',
},
},
},
},
{
actions: {
storeSearch: assign((_context, event) => ({
// Store search from search event
...('search' in event && { search: event.search }),
})),
storeDataViews: assign((_context, event) =>
'data' in event && !isError(event.data)
? { dataViewsSource: event.data, dataViews: event.data }
: {}
),
searchDataViews: assign((context) => {
if (context.dataViewsSource !== null) {
return {
dataViews: searchDataViews(context.dataViewsSource, context.search),
};
}
return {};
}),
storeInCache: (context, event) => {
if ('data' in event && !isError(event.data)) {
context.cache.set(context.search, event.data);
}
},
storeError: assign((_context, event) =>
'data' in event && isError(event.data) ? { error: event.data } : {}
),
clearCache: (context) => {
context.cache.reset();
},
clearData: assign((_context) => ({ dataViews: null })),
clearError: assign((_context) => ({ error: null })),
},
}
);
export interface DataViewsStateMachineDependencies {
initialContext?: DefaultDataViewsContext;
dataViews: DataViewsPublicPluginStart;
discover: DiscoverStart;
}
export const createDataViewsStateMachine = ({
initialContext,
dataViews,
discover,
}: DataViewsStateMachineDependencies) =>
createPureDataViewsStateMachine(initialContext).withConfig({
actions: {
navigateToDiscoverDataView: (_context, event) => {
if (event.type === 'SELECT_DATA_VIEW' && 'dataView' in event) {
discover.locator?.navigate({ dataViewId: event.dataView.id });
}
},
},
services: {
loadDataViews: (context) => {
const searchParams = context.search;
return context.cache.has(searchParams)
? Promise.resolve(context.cache.get(searchParams))
: dataViews.getIdsWithTitle().then((views) => views.map(parseDataViewListItem));
},
},
});
const searchDataViews = (dataViews: DataViewListItem[], search: DataViewsSearchParams) => {
const { name, sortOrder } = search;
return dataViews
.filter((dataView) => Boolean(dataView.name?.includes(name ?? '')))
.sort(createComparatorByField<DataViewListItem>('name', sortOrder));
};

View file

@ -0,0 +1,101 @@
/*
* 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 { DoneInvokeEvent } from 'xstate';
import type { DataViewListItem } from '@kbn/data-views-plugin/common';
import type { IHashedCache } from '../../../../common/hashed_cache';
import { SortOrder } from '../../../../common/latest';
export interface DataViewsSearchParams {
name?: string;
sortOrder?: SortOrder;
}
export interface WithCache {
cache: IHashedCache<DataViewsSearchParams, DataViewListItem[]>;
}
export interface WithSearch {
search: DataViewsSearchParams;
}
export interface WithDataViews {
dataViewsSource: DataViewListItem[];
dataViews: DataViewListItem[];
}
export interface WithNullishDataViews {
dataViewsSource: null;
dataViews: null;
}
export interface WithError {
error: Error;
}
export interface WithNullishError {
error: null;
}
export type DefaultDataViewsContext = WithCache &
WithNullishDataViews &
WithSearch &
WithNullishError;
type LoadingDataViewsContext = DefaultDataViewsContext;
type LoadedDataViewsContext = WithCache & WithDataViews & WithSearch & WithNullishError;
type LoadingFailedDataViewsContext = WithCache & WithNullishDataViews & WithSearch & WithError;
export type DataViewsTypestate =
| {
value: 'uninitialized';
context: DefaultDataViewsContext;
}
| {
value: 'loading';
context: LoadingDataViewsContext;
}
| {
value: 'loaded';
context: LoadedDataViewsContext;
}
| {
value: 'loaded.idle';
context: LoadedDataViewsContext;
}
| {
value: 'loaded.debounceSearchingDataViews';
context: LoadedDataViewsContext;
}
| {
value: 'loadingFailed';
context: LoadingFailedDataViewsContext;
};
export type DataViewsContext = DataViewsTypestate['context'];
export type DataViewsEvent =
| {
type: 'LOAD_DATA_VIEWS';
}
| {
type: 'RELOAD_DATA_VIEWS';
}
| {
type: 'SELECT_DATA_VIEW';
dataView: DataViewListItem;
}
| {
type: 'SEARCH_DATA_VIEWS';
search: DataViewsSearchParams;
}
| {
type: 'SORT_DATA_VIEWS';
search: DataViewsSearchParams;
}
| DoneInvokeEvent<DataViewListItem[] | Error>;

View file

@ -4,10 +4,11 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { ComponentType } from 'react';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import type { DiscoverSetup, DiscoverStart } from '@kbn/discover-plugin/public';
import { SharePluginSetup } from '@kbn/share-plugin/public';
import type { ComponentType } from 'react';
import { LogExplorerLocators } from '../common/locators';
import type { LogExplorerProps } from './components/log_explorer';
@ -25,5 +26,6 @@ export interface LogExplorerSetupDeps {
export interface LogExplorerStartDeps {
data: DataPublicPluginStart;
dataViews: DataViewsPublicPluginStart;
discover: DiscoverStart;
}

View file

@ -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.
*/
const BASE_DATA_VIEW_TEST_SUBJ = 'logExplorerDataView';
const publicDataViewPatternsSet = new Set(['logs-*', 'logstash-*', 'filebeat-*']);
export const getDataViewTestSubj = (title: string) => {
if (publicDataViewPatternsSet.has(title)) {
return [BASE_DATA_VIEW_TEST_SUBJ, cleanTitle(title)].join('_');
}
return BASE_DATA_VIEW_TEST_SUBJ;
};
const cleanTitle = (title: string) => title.slice(0, -2);

View file

@ -0,0 +1,15 @@
/*
* 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 { DataViewListItem } from '@kbn/data-views-plugin/common';
export const parseDataViewListItem = (dataViewListItem: DataViewListItem) => {
return {
...dataViewListItem,
name: dataViewListItem.name ?? dataViewListItem.title,
};
};

View file

@ -25,7 +25,7 @@ export const betaBadgeDescription = i18n.translate(
export const discoverLinkTitle = i18n.translate(
'xpack.observabilityLogExplorer.discoverLinkTitle',
{
defaultMessage: 'Discover',
defaultMessage: 'Open in Discover',
}
);

View file

@ -14,23 +14,32 @@ const initialPackageMap = {
};
const initialPackagesTexts = Object.values(initialPackageMap);
const expectedDataViews = ['logstash-*', 'logs-*', 'metrics-*'];
const sortedExpectedDataViews = expectedDataViews.slice().sort();
const uncategorized = ['logs-gaming-*', 'logs-manufacturing-*', 'logs-retail-*'];
const expectedUncategorized = uncategorized.map((dataset) => dataset.split('-')[1]);
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const browser = getService('browser');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const retry = getService('retry');
const PageObjects = getPageObjects(['common', 'observabilityLogExplorer']);
const PageObjects = getPageObjects(['common', 'discover', 'observabilityLogExplorer']);
const noIntegrationsTitle = 'No integrations found';
const noUncategorizedTitle = 'No data streams found';
describe('Dataset Selector', () => {
before(async () => {
await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover');
await PageObjects.observabilityLogExplorer.removeInstalledPackages();
});
after(async () => {
await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover');
});
describe('as consistent behavior', () => {
before(async () => {
await PageObjects.observabilityLogExplorer.navigateTo();
@ -41,14 +50,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.observabilityLogExplorer.openDatasetSelector();
});
it('should always display the Integrations and Uncategorized top level tabs', async () => {
it('should always display the Integrations Uncategorized and Data Views top level tabs', async () => {
const integrationsTab = await PageObjects.observabilityLogExplorer.getIntegrationsTab();
const uncategorizedTab = await PageObjects.observabilityLogExplorer.getUncategorizedTab();
const dataViewsTab = await PageObjects.observabilityLogExplorer.getDataViewsTab();
expect(await integrationsTab.isDisplayed()).to.be(true);
expect(await integrationsTab.getVisibleText()).to.be('Integrations');
expect(await uncategorizedTab.isDisplayed()).to.be(true);
expect(await uncategorizedTab.getVisibleText()).to.be('Uncategorized');
expect(await dataViewsTab.isDisplayed()).to.be(true);
expect(await dataViewsTab.getVisibleText()).to.be('Data Views');
});
it('should always display the "Show all logs" action', async () => {
@ -565,6 +577,143 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
});
describe('when open on the data views tab', () => {
before(async () => {
await PageObjects.observabilityLogExplorer.navigateTo();
});
beforeEach(async () => {
await browser.refresh();
await PageObjects.observabilityLogExplorer.openDatasetSelector();
await PageObjects.observabilityLogExplorer.getDataViewsTab().then((tab) => tab.click());
});
it('should display a list of available data views', async () => {
await retry.try(async () => {
const [panelTitleNode, menuEntries] = await PageObjects.observabilityLogExplorer
.getDataViewsContextMenu()
.then((menu) =>
Promise.all([
PageObjects.observabilityLogExplorer.getPanelTitle(menu),
PageObjects.observabilityLogExplorer.getPanelEntries(menu),
])
);
expect(
await PageObjects.observabilityLogExplorer.getDataViewsContextMenuTitle(
panelTitleNode
)
).to.be('Data Views');
expect(await menuEntries[0].getVisibleText()).to.be(expectedDataViews[0]);
expect(await menuEntries[1].getVisibleText()).to.be(expectedDataViews[1]);
expect(await menuEntries[2].getVisibleText()).to.be(expectedDataViews[2]);
});
});
it('should sort the data views list by the clicked sorting option', async () => {
await retry.try(async () => {
const panelTitleNode = await PageObjects.observabilityLogExplorer
.getDataViewsContextMenu()
.then((menu) => PageObjects.observabilityLogExplorer.getPanelTitle(menu));
expect(
await PageObjects.observabilityLogExplorer.getDataViewsContextMenuTitle(
panelTitleNode
)
).to.be('Data Views');
});
// Test descending order
await PageObjects.observabilityLogExplorer.clickSortButtonBy('desc');
await retry.try(async () => {
const menuEntries = await PageObjects.observabilityLogExplorer
.getDataViewsContextMenu()
.then((menu) => PageObjects.observabilityLogExplorer.getPanelEntries(menu));
expect(await menuEntries[0].getVisibleText()).to.be(sortedExpectedDataViews[2]);
expect(await menuEntries[1].getVisibleText()).to.be(sortedExpectedDataViews[1]);
expect(await menuEntries[2].getVisibleText()).to.be(sortedExpectedDataViews[0]);
});
// Test back ascending order
await PageObjects.observabilityLogExplorer.clickSortButtonBy('asc');
await retry.try(async () => {
const menuEntries = await PageObjects.observabilityLogExplorer
.getDataViewsContextMenu()
.then((menu) => PageObjects.observabilityLogExplorer.getPanelEntries(menu));
expect(await menuEntries[0].getVisibleText()).to.be(sortedExpectedDataViews[0]);
expect(await menuEntries[1].getVisibleText()).to.be(sortedExpectedDataViews[1]);
expect(await menuEntries[2].getVisibleText()).to.be(sortedExpectedDataViews[2]);
});
});
it('should filter the datasets list by the typed data view name', async () => {
await retry.try(async () => {
const panelTitleNode = await PageObjects.observabilityLogExplorer
.getDataViewsContextMenu()
.then((menu) => PageObjects.observabilityLogExplorer.getPanelTitle(menu));
expect(
await PageObjects.observabilityLogExplorer.getDataViewsContextMenuTitle(
panelTitleNode
)
).to.be('Data Views');
});
await retry.try(async () => {
const menuEntries = await PageObjects.observabilityLogExplorer
.getDataViewsContextMenu()
.then((menu) => PageObjects.observabilityLogExplorer.getPanelEntries(menu));
expect(await menuEntries[0].getVisibleText()).to.be(expectedDataViews[0]);
expect(await menuEntries[1].getVisibleText()).to.be(expectedDataViews[1]);
expect(await menuEntries[2].getVisibleText()).to.be(expectedDataViews[2]);
});
await PageObjects.observabilityLogExplorer.typeSearchFieldWith('logs');
await retry.try(async () => {
const menuEntries = await PageObjects.observabilityLogExplorer
.getDataViewsContextMenu()
.then((menu) => PageObjects.observabilityLogExplorer.getPanelEntries(menu));
expect(menuEntries.length).to.be(2);
expect(await menuEntries[0].getVisibleText()).to.be('logs-*');
expect(await menuEntries[1].getVisibleText()).to.be('logstash-*');
});
});
it('should navigate to Discover with the clicked data view preselected', async () => {
await retry.try(async () => {
const panelTitleNode = await PageObjects.observabilityLogExplorer
.getDataViewsContextMenu()
.then((menu) => PageObjects.observabilityLogExplorer.getPanelTitle(menu));
expect(
await PageObjects.observabilityLogExplorer.getDataViewsContextMenuTitle(
panelTitleNode
)
).to.be('Data Views');
});
await retry.try(async () => {
const menuEntries = await PageObjects.observabilityLogExplorer
.getDataViewsContextMenu()
.then((menu) => PageObjects.observabilityLogExplorer.getPanelEntries(menu));
expect(await menuEntries[2].getVisibleText()).to.be(expectedDataViews[2]);
menuEntries[2].click();
});
await retry.try(async () => {
expect(await PageObjects.discover.getCurrentlySelectedDataView()).to.eql(
expectedDataViews[2]
);
});
});
});
describe('when open/close the selector', () => {
before(async () => {
await PageObjects.observabilityLogExplorer.navigateTo();

View file

@ -208,6 +208,18 @@ export function ObservabilityLogExplorerPageObject({
return testSubjects.find('datasetSelectorUncategorizedTab');
},
getDataViewsContextMenu() {
return testSubjects.find('dataViewsContextMenu');
},
getDataViewsContextMenuTitle(panelTitleNode: WebElementWrapper) {
return panelTitleNode.getVisibleText().then((title) => title.split('\n')[0]);
},
getDataViewsTab() {
return testSubjects.find('datasetSelectorDataViewsTab');
},
getPanelTitle(contextMenu: WebElementWrapper) {
return contextMenu.findByClassName('euiContextMenuPanelTitle');
},

View file

@ -14,6 +14,9 @@ const initialPackageMap = {
};
const initialPackagesTexts = Object.values(initialPackageMap);
const expectedDataViews = ['logs-*', 'metrics-*'];
const sortedExpectedDataViews = expectedDataViews.slice().sort();
const uncategorized = ['logs-gaming-*', 'logs-manufacturing-*', 'logs-retail-*'];
const expectedUncategorized = uncategorized.map((dataset) => dataset.split('-')[1]);
@ -21,7 +24,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const browser = getService('browser');
const esArchiver = getService('esArchiver');
const retry = getService('retry');
const PageObjects = getPageObjects(['common', 'observabilityLogExplorer', 'svlCommonPage']);
const PageObjects = getPageObjects([
'common',
'discover',
'observabilityLogExplorer',
'svlCommonPage',
]);
const noIntegrationsTitle = 'No integrations found';
const noUncategorizedTitle = 'No data streams found';
@ -46,14 +54,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.observabilityLogExplorer.openDatasetSelector();
});
it('should always display the Integrations and Uncategorized top level tabs', async () => {
it('should always display the Integrations Uncategorized and Data Views top level tabs', async () => {
const integrationsTab = await PageObjects.observabilityLogExplorer.getIntegrationsTab();
const uncategorizedTab = await PageObjects.observabilityLogExplorer.getUncategorizedTab();
const dataViewsTab = await PageObjects.observabilityLogExplorer.getDataViewsTab();
expect(await integrationsTab.isDisplayed()).to.be(true);
expect(await integrationsTab.getVisibleText()).to.be('Integrations');
expect(await uncategorizedTab.isDisplayed()).to.be(true);
expect(await uncategorizedTab.getVisibleText()).to.be('Uncategorized');
expect(await dataViewsTab.isDisplayed()).to.be(true);
expect(await dataViewsTab.getVisibleText()).to.be('Data Views');
});
it('should always display the "Show all logs" action', async () => {
@ -570,6 +581,138 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
});
describe('when open on the data views tab', () => {
before(async () => {
await PageObjects.observabilityLogExplorer.navigateTo();
});
beforeEach(async () => {
await browser.refresh();
await PageObjects.observabilityLogExplorer.openDatasetSelector();
await PageObjects.observabilityLogExplorer.getDataViewsTab().then((tab) => tab.click());
});
it('should display a list of available data views', async () => {
await retry.try(async () => {
const [panelTitleNode, menuEntries] = await PageObjects.observabilityLogExplorer
.getDataViewsContextMenu()
.then((menu) =>
Promise.all([
PageObjects.observabilityLogExplorer.getPanelTitle(menu),
PageObjects.observabilityLogExplorer.getPanelEntries(menu),
])
);
expect(
await PageObjects.observabilityLogExplorer.getDataViewsContextMenuTitle(
panelTitleNode
)
).to.be('Data Views');
expect(await menuEntries[0].getVisibleText()).to.be(expectedDataViews[0]);
expect(await menuEntries[1].getVisibleText()).to.be(expectedDataViews[1]);
});
});
it('should sort the data views list by the clicked sorting option', async () => {
await retry.try(async () => {
const panelTitleNode = await PageObjects.observabilityLogExplorer
.getDataViewsContextMenu()
.then((menu) => PageObjects.observabilityLogExplorer.getPanelTitle(menu));
expect(
await PageObjects.observabilityLogExplorer.getDataViewsContextMenuTitle(
panelTitleNode
)
).to.be('Data Views');
});
// Test descending order
await PageObjects.observabilityLogExplorer.clickSortButtonBy('desc');
await retry.try(async () => {
const menuEntries = await PageObjects.observabilityLogExplorer
.getDataViewsContextMenu()
.then((menu) => PageObjects.observabilityLogExplorer.getPanelEntries(menu));
expect(await menuEntries[0].getVisibleText()).to.be(sortedExpectedDataViews[1]);
expect(await menuEntries[1].getVisibleText()).to.be(sortedExpectedDataViews[0]);
});
// Test back ascending order
await PageObjects.observabilityLogExplorer.clickSortButtonBy('asc');
await retry.try(async () => {
const menuEntries = await PageObjects.observabilityLogExplorer
.getDataViewsContextMenu()
.then((menu) => PageObjects.observabilityLogExplorer.getPanelEntries(menu));
expect(await menuEntries[0].getVisibleText()).to.be(sortedExpectedDataViews[0]);
expect(await menuEntries[1].getVisibleText()).to.be(sortedExpectedDataViews[1]);
});
});
it('should filter the datasets list by the typed data view name', async () => {
await retry.try(async () => {
const panelTitleNode = await PageObjects.observabilityLogExplorer
.getDataViewsContextMenu()
.then((menu) => PageObjects.observabilityLogExplorer.getPanelTitle(menu));
expect(
await PageObjects.observabilityLogExplorer.getDataViewsContextMenuTitle(
panelTitleNode
)
).to.be('Data Views');
});
await retry.try(async () => {
const menuEntries = await PageObjects.observabilityLogExplorer
.getDataViewsContextMenu()
.then((menu) => PageObjects.observabilityLogExplorer.getPanelEntries(menu));
expect(await menuEntries[0].getVisibleText()).to.be(expectedDataViews[0]);
expect(await menuEntries[1].getVisibleText()).to.be(expectedDataViews[1]);
});
await PageObjects.observabilityLogExplorer.typeSearchFieldWith('logs');
await retry.try(async () => {
const menuEntries = await PageObjects.observabilityLogExplorer
.getDataViewsContextMenu()
.then((menu) => PageObjects.observabilityLogExplorer.getPanelEntries(menu));
expect(menuEntries.length).to.be(1);
expect(await menuEntries[0].getVisibleText()).to.be('logs-*');
});
});
it('should navigate to Discover with the clicked data view preselected', async () => {
await retry.try(async () => {
const panelTitleNode = await PageObjects.observabilityLogExplorer
.getDataViewsContextMenu()
.then((menu) => PageObjects.observabilityLogExplorer.getPanelTitle(menu));
expect(
await PageObjects.observabilityLogExplorer.getDataViewsContextMenuTitle(
panelTitleNode
)
).to.be('Data Views');
});
await retry.try(async () => {
const menuEntries = await PageObjects.observabilityLogExplorer
.getDataViewsContextMenu()
.then((menu) => PageObjects.observabilityLogExplorer.getPanelEntries(menu));
expect(await menuEntries[1].getVisibleText()).to.be(expectedDataViews[1]);
menuEntries[1].click();
});
await retry.try(async () => {
expect(await PageObjects.discover.getCurrentlySelectedDataView()).to.eql(
expectedDataViews[1]
);
});
});
});
describe('when open/close the selector', () => {
before(async () => {
await PageObjects.observabilityLogExplorer.navigateTo();