Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}>;
}
Original file line number Diff line number Diff line change
@@ -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>
);
}
);
Original file line number Diff line number Diff line change
@@ -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}`);
}
Original file line number Diff line number Diff line change
@@ -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} />;
});
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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-*-*';

Expand Down Expand Up @@ -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 },
Expand All @@ -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 : '',
Comment on lines +146 to +147
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After testing locally, I realized we still display the package id instead of the nice name (system vs System). Now that we have the package saved objects here, what do you think about restructuring DataStream type to include package name?:

package: {
  id: string;
  name: string;
  version: string;
}

Not a blocker for FF tho :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we should do that for beta1. We'll need to add the title from the registry package information to our epm-packages saved object type, and I'm reluctant to do that right now, so this is #65230 now.

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({
Expand All @@ -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);
};
23 changes: 18 additions & 5 deletions x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -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);
}