From 3be21c9e562dbc420d394c253995b696bb4e3147 Mon Sep 17 00:00:00 2001 From: Marco Antonio Ghiani Date: Thu, 28 Sep 2023 12:21:35 +0200 Subject: [PATCH] [Log Explorer] Implement Data Views tab into selector (#166938) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 📓 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. https://github.com/elastic/kibana/assets/34506779/e8d1f622-86fb-4841-b4cc-4a913067d2cc ## Updated selector state machine Screenshot 2023-09-22 at 12 15 44 ## New DataViews state machine Screenshot 2023-09-22 at 12 39 09 --------- Co-authored-by: Marco Antonio Ghiani Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../common/dataset_selection/index.ts | 2 + .../components/dataset_selector/constants.tsx | 23 ++- .../dataset_selector.stories.tsx | 66 +++++++- .../dataset_selector/dataset_selector.tsx | 91 ++++++++-- .../state_machine/state_machine.ts | 80 +++++++-- .../dataset_selector/state_machine/types.ts | 28 ++- .../state_machine/use_dataset_selector.ts | 31 +++- .../sub_components/data_views_panel_title.tsx | 21 +++ .../sub_components/list_status.tsx | 3 +- .../components/dataset_selector/types.ts | 49 ++++-- .../components/dataset_selector/utils.tsx | 18 ++ .../components/log_explorer/log_explorer.tsx | 27 +-- .../custom_dataset_selector.tsx | 44 ++++- .../customizations/log_explorer_profile.tsx | 10 +- .../public/hooks/use_data_views.tsx | 104 ++++++++++++ x-pack/plugins/log_explorer/public/plugin.ts | 5 +- .../public/state_machines/data_views/index.ts | 8 + .../state_machines/data_views/src/defaults.ts | 20 +++ .../state_machines/data_views/src/index.ts | 9 + .../data_views/src/state_machine.ts | 160 ++++++++++++++++++ .../state_machines/data_views/src/types.ts | 101 +++++++++++ x-pack/plugins/log_explorer/public/types.ts | 4 +- .../public/utils/get_data_view_test_subj.ts | 20 +++ .../public/utils/parse_data_view_list_item.ts | 15 ++ .../common/translations.ts | 2 +- .../dataset_selector.ts | 153 ++++++++++++++++- .../observability_log_explorer.ts | 12 ++ .../dataset_selector.ts | 147 +++++++++++++++- 28 files changed, 1161 insertions(+), 92 deletions(-) create mode 100644 x-pack/plugins/log_explorer/public/components/dataset_selector/sub_components/data_views_panel_title.tsx create mode 100644 x-pack/plugins/log_explorer/public/hooks/use_data_views.tsx create mode 100644 x-pack/plugins/log_explorer/public/state_machines/data_views/index.ts create mode 100644 x-pack/plugins/log_explorer/public/state_machines/data_views/src/defaults.ts create mode 100644 x-pack/plugins/log_explorer/public/state_machines/data_views/src/index.ts create mode 100644 x-pack/plugins/log_explorer/public/state_machines/data_views/src/state_machine.ts create mode 100644 x-pack/plugins/log_explorer/public/state_machines/data_views/src/types.ts create mode 100644 x-pack/plugins/log_explorer/public/utils/get_data_view_test_subj.ts create mode 100644 x-pack/plugins/log_explorer/public/utils/parse_data_view_list_item.ts diff --git a/x-pack/plugins/log_explorer/common/dataset_selection/index.ts b/x-pack/plugins/log_explorer/common/dataset_selection/index.ts index 3284610f53bcc..f390f7a89f87c 100644 --- a/x-pack/plugins/log_explorer/common/dataset_selection/index.ts +++ b/x-pack/plugins/log_explorer/common/dataset_selection/index.ts @@ -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 ( diff --git a/x-pack/plugins/log_explorer/public/components/dataset_selector/constants.tsx b/x-pack/plugins/log_explorer/public/components/dataset_selector/constants.tsx index 1cbcd6a0f032a..28a5401f98048 100644 --- a/x-pack/plugins/log_explorer/public/components/dataset_selector/constants.tsx +++ b/x-pack/plugins/log_explorer/public/components/dataset_selector/constants.tsx @@ -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' } diff --git a/x-pack/plugins/log_explorer/public/components/dataset_selector/dataset_selector.stories.tsx b/x-pack/plugins/log_explorer/public/components/dataset_selector/dataset_selector.stories.tsx index 10bc958c8f2ce..82178164994eb 100644 --- a/x-pack/plugins/log_explorer/public/components/dataset_selector/dataset_selector.stories.tsx +++ b/x-pack/plugins/log_explorer/public/components/dataset_selector/dataset_selector.stories.tsx @@ -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 = { 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 = (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 ( ); }; @@ -92,12 +116,18 @@ const DatasetSelectorTemplate: Story = (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', + }, +]; diff --git a/x-pack/plugins/log_explorer/public/components/dataset_selector/dataset_selector.tsx b/x-pack/plugins/log_explorer/public/components/dataset_selector/dataset_selector.tsx index f9e722effc784..10d8b4c046c9a 100644 --- a/x-pack/plugins/log_explorer/public/components/dataset_selector/dataset_selector.tsx +++ b/x-pack/plugins/log_explorer/public/components/dataset_selector/dataset_selector.tsx @@ -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} /> {/* 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 */} +