diff --git a/x-pack/plugins/ingest_manager/common/types/models/data_stream.ts b/x-pack/plugins/ingest_manager/common/types/models/data_stream.ts index 7da9bbad1b170..abc9ffcf6be6a 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/data_stream.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/data_stream.ts @@ -10,6 +10,11 @@ export interface DataStream { namespace: string; type: string; package: string; + package_version: string; last_activity: string; size_in_bytes: number; + dashboards: Array<{ + id: string; + title: string; + }>; } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/table_row_actions_nested.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/table_row_actions_nested.tsx new file mode 100644 index 0000000000000..56f010e2fa774 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/table_row_actions_nested.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, useState } from 'react'; +import { EuiButtonIcon, EuiContextMenu, EuiPopover } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EuiContextMenuProps } from '@elastic/eui/src/components/context_menu/context_menu'; + +export const TableRowActionsNested = React.memo<{ panels: EuiContextMenuProps['panels'] }>( + ({ panels }) => { + const [isOpen, setIsOpen] = useState(false); + const handleCloseMenu = useCallback(() => setIsOpen(false), [setIsOpen]); + const handleToggleMenu = useCallback(() => setIsOpen(!isOpen), [isOpen]); + + return ( + + } + isOpen={isOpen} + closePopover={handleCloseMenu} + > + + + ); + } +); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_kibana_link.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_kibana_link.ts new file mode 100644 index 0000000000000..f6c5b8bc03fce --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_kibana_link.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useCore } from './'; + +const BASE_PATH = '/app/kibana'; + +export function useKibanaLink(path: string = '/') { + const core = useCore(); + return core.http.basePath.prepend(`${BASE_PATH}#${path}`); +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/components/data_stream_row_actions.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/components/data_stream_row_actions.tsx new file mode 100644 index 0000000000000..ac47387cd7ab3 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/components/data_stream_row_actions.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo } from 'react'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { useKibanaLink } from '../../../../hooks/use_kibana_link'; +import { DataStream } from '../../../../types'; +import { TableRowActionsNested } from '../../../../components/table_row_actions_nested'; + +export const DataStreamRowActions = memo<{ datastream: DataStream }>(({ datastream }) => { + const { dashboards } = datastream; + const panels = []; + const actionNameSingular = ( + + ); + const actionNamePlural = ( + + ); + + const panelTitle = i18n.translate('xpack.ingestManager.dataStreamList.viewDashboardsPanelTitle', { + defaultMessage: 'View dashboards', + }); + + if (!dashboards || dashboards.length === 0) { + panels.push({ + id: 0, + items: [ + { + icon: 'dashboardApp', + disabled: true, + name: actionNameSingular, + }, + ], + }); + } else if (dashboards.length === 1) { + panels.push({ + id: 0, + items: [ + { + icon: 'dashboardApp', + href: useKibanaLink(`/dashboard/${dashboards[0].id || ''}`), + name: actionNameSingular, + }, + ], + }); + } else { + panels.push({ + id: 0, + items: [ + { + icon: 'dashboardApp', + panel: 1, + name: actionNamePlural, + }, + ], + }); + panels.push({ + id: 1, + title: panelTitle, + items: dashboards.map(dashboard => { + return { + icon: 'dashboardApp', + href: useKibanaLink(`/dashboard/${dashboard.id || ''}`), + name: dashboard.title, + }; + }), + }); + } + + return ; +}); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx index d7a3e933f3bb5..cff138c6a16ca 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx @@ -20,6 +20,8 @@ import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; import { DataStream } from '../../../types'; import { WithHeaderLayout } from '../../../layouts'; import { useGetDataStreams, useStartDeps, usePagination } from '../../../hooks'; +import { PackageIcon } from '../../../components/package_icon'; +import { DataStreamRowActions } from './components/data_stream_row_actions'; const DataStreamListPageLayout: React.FunctionComponent = ({ children }) => ( = () => { const { pagination, pageSizeOptions } = usePagination(); - // Fetch agent configs + // Fetch data streams const { isLoading, data: dataStreamsData, sendRequest } = useGetDataStreams(); // Some configs retrieved, set up table props @@ -102,6 +104,23 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { name: i18n.translate('xpack.ingestManager.dataStreamList.integrationColumnTitle', { defaultMessage: 'Integration', }), + render(pkg: DataStream['package'], datastream: DataStream) { + return ( + + {datastream.package_version && ( + + + + )} + {pkg} + + ); + }, }, { field: 'last_activity', @@ -135,6 +154,16 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { } }, }, + { + name: i18n.translate('xpack.ingestManager.dataStreamList.actionsColumnTitle', { + defaultMessage: 'Actions', + }), + actions: [ + { + render: (datastream: DataStream) => , + }, + ], + }, ]; return cols; }, [fieldFormats]); diff --git a/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts index 0d2909edf00c4..ad81076e34e4b 100644 --- a/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts @@ -3,9 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandler } from 'src/core/server'; +import { RequestHandler, SavedObjectsClientContract } from 'src/core/server'; import { DataStream } from '../../types'; -import { GetDataStreamsResponse } from '../../../common'; +import { GetDataStreamsResponse, KibanaAssetType } from '../../../common'; +import { getPackageSavedObjects, getKibanaSavedObject } from '../../services/epm/packages/get'; const DATA_STREAM_INDEX_PATTERN = 'logs-*-*,metrics-*-*'; @@ -100,7 +101,10 @@ export const getListHandler: RequestHandler = async (context, request, response) index: { buckets: indexResults }, } = aggregations; - const dataStreams: DataStream[] = (indexResults as any[]).map(result => { + const packageSavedObjects = await getPackageSavedObjects(context.core.savedObjects.client); + const packageMetadata: any = {}; + + const dataStreamsPromises = (indexResults as any[]).map(async result => { const { key: indexName, dataset: { buckets: datasetBuckets }, @@ -109,17 +113,46 @@ export const getListHandler: RequestHandler = async (context, request, response) package: { buckets: packageBuckets }, last_activity: { value_as_string: lastActivity }, } = result; + + const pkg = packageBuckets.length ? packageBuckets[0].key : ''; + const pkgSavedObject = packageSavedObjects.saved_objects.filter(p => p.id === pkg); + + // if + // - the datastream is associated with a package + // - and the package has been installed through EPM + // - and we didn't pick the metadata in an earlier iteration of this map() + if (pkg !== '' && pkgSavedObject.length > 0 && !packageMetadata[pkg]) { + // then pick the dashboards from the package saved object + const dashboards = + pkgSavedObject[0].attributes?.installed?.filter( + o => o.type === KibanaAssetType.dashboard + ) || []; + // and then pick the human-readable titles from the dashboard saved objects + const enhancedDashboards = await getEnhancedDashboards( + context.core.savedObjects.client, + dashboards + ); + + packageMetadata[pkg] = { + version: pkgSavedObject[0].attributes?.version || '', + dashboards: enhancedDashboards, + }; + } return { index: indexName, dataset: datasetBuckets.length ? datasetBuckets[0].key : '', namespace: namespaceBuckets.length ? namespaceBuckets[0].key : '', type: typeBuckets.length ? typeBuckets[0].key : '', - package: packageBuckets.length ? packageBuckets[0].key : '', + package: pkg, + package_version: packageMetadata[pkg] ? packageMetadata[pkg].version : '', last_activity: lastActivity, size_in_bytes: indexStats[indexName] ? indexStats[indexName].total.store.size_in_bytes : 0, + dashboards: packageMetadata[pkg] ? packageMetadata[pkg].dashboards : [], }; }); + const dataStreams: DataStream[] = await Promise.all(dataStreamsPromises); + body.data_streams = dataStreams; return response.ok({ @@ -132,3 +165,21 @@ export const getListHandler: RequestHandler = async (context, request, response) }); } }; + +const getEnhancedDashboards = async ( + savedObjectsClient: SavedObjectsClientContract, + dashboards: any[] +) => { + const dashboardsPromises = dashboards.map(async db => { + const dbSavedObject: any = await getKibanaSavedObject( + savedObjectsClient, + KibanaAssetType.dashboard, + db.id + ); + return { + id: db.id, + title: dbSavedObject.attributes?.title || db.id, + }; + }); + return await Promise.all(dashboardsPromises); +}; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts index da8d79a04b97c..6db08e344b3da 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts @@ -6,7 +6,7 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; -import { Installation, InstallationStatus, PackageInfo } from '../../../types'; +import { Installation, InstallationStatus, PackageInfo, KibanaAssetType } from '../../../types'; import * as Registry from '../registry'; import { createInstallableFrom } from './index'; @@ -32,11 +32,10 @@ export async function getPackages( ); }); // get the installed packages - const results = await savedObjectsClient.find({ - type: PACKAGES_SAVED_OBJECT_TYPE, - }); + const packageSavedObjects = await getPackageSavedObjects(savedObjectsClient); + // filter out any internal packages - const savedObjectsVisible = results.saved_objects.filter(o => !o.attributes.internal); + const savedObjectsVisible = packageSavedObjects.saved_objects.filter(o => !o.attributes.internal); const packageList = registryItems .map(item => createInstallableFrom( @@ -48,6 +47,12 @@ export async function getPackages( return packageList; } +export async function getPackageSavedObjects(savedObjectsClient: SavedObjectsClientContract) { + return savedObjectsClient.find({ + type: PACKAGES_SAVED_OBJECT_TYPE, + }); +} + export async function getPackageKeysByStatus( savedObjectsClient: SavedObjectsClientContract, status: InstallationStatus @@ -114,3 +119,11 @@ function sortByName(a: { name: string }, b: { name: string }) { return 0; } } + +export async function getKibanaSavedObject( + savedObjectsClient: SavedObjectsClientContract, + type: KibanaAssetType, + id: string +) { + return savedObjectsClient.get(type, id); +}