[Ingest] Datastream list: add icons and dashboard links (#65048)
* Read package saved objects in data stream handler. * Render package icon. * Make TableRowAction more generic * Add Actions column to data stream list * Disable dashboard link if no dashboards present. * Data stream list: link to first dashbord found * Update i18n strings * Add nested context menu to link to dashboards * introduces a separate TableRowActionsNested component * moves TableRowActions back into agent config components * Fix i18n label. * Re-add translated strings removed by mistake * Fix i18n issues * Add helper to read a saved object installed by EPM * Display titles from within dashboard saved objects
This commit is contained in:
parent
ba3534eca3
commit
4142f575e1
|
@ -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;
|
||||
}>;
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<EuiPopover
|
||||
anchorPosition="downRight"
|
||||
panelPaddingSize="none"
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
iconType="boxesHorizontal"
|
||||
onClick={handleToggleMenu}
|
||||
aria-label={i18n.translate('xpack.ingestManager.genericActionsMenuText', {
|
||||
defaultMessage: 'Open',
|
||||
})}
|
||||
/>
|
||||
}
|
||||
isOpen={isOpen}
|
||||
closePopover={handleCloseMenu}
|
||||
>
|
||||
<EuiContextMenu panels={panels} initialPanelId={0} />
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -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}`);
|
||||
}
|
|
@ -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 = (
|
||||
<FormattedMessage
|
||||
id="xpack.ingestManager.dataStreamList.viewDashboardActionText"
|
||||
defaultMessage="View dashboard"
|
||||
/>
|
||||
);
|
||||
const actionNamePlural = (
|
||||
<FormattedMessage
|
||||
id="xpack.ingestManager.dataStreamList.viewDashboardsActionText"
|
||||
defaultMessage="View dashboards"
|
||||
/>
|
||||
);
|
||||
|
||||
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 <TableRowActionsNested panels={panels} />;
|
||||
});
|
|
@ -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 }) => (
|
||||
<WithHeaderLayout
|
||||
|
@ -59,7 +61,7 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => {
|
|||
|
||||
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 (
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
{datastream.package_version && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<PackageIcon
|
||||
packageName={pkg}
|
||||
version={datastream.package_version}
|
||||
size="m"
|
||||
tryApi={true}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>{pkg}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
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) => <DataStreamRowActions datastream={datastream} />,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
return cols;
|
||||
}, [fieldFormats]);
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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<Installation>({
|
||||
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<Installation>({
|
||||
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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue