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);
+}