[Fleet] Surface new overview dashboards in fleet (#154914)

Closes https://github.com/elastic/kibana/issues/153848

## Summary
- Adds two link buttons on top of agent list page to access "Ingest
overview" and "Agent Info" dashboards
<img width="1444" alt="Screenshot 2023-04-13 at 15 22 53"
src="https://user-images.githubusercontent.com/16084106/231772174-00c00a8e-62f1-43ea-a935-bc12f56f3e50.png">

The links are built using the new URL service
[locator](e80abe8108/x-pack/plugins/fleet/public/hooks/use_locator.ts (L14))
and the
[getRedirectLink](https://github.com/elastic/kibana/blob/main/src/plugins/share/README.mdx#using-locator-of-another-app)
method;

- Refactoring existing instances of `useKibanaLink` to use the url
locator instead;

These new dashboards were already accessible from the ` elastic_agent.*`
datastreams table actions, however I replaced the `useKibanaLink` hook
there as well:

<img width="1412" alt="Screenshot 2023-04-13 at 16 03 47"
src="https://user-images.githubusercontent.com/16084106/231784273-693c7f36-4545-4c06-a05e-4f09e53bd903.png">


TODO: I don't know where to add the "Integrations" dashboard yet, I'm
not sure it should go on the Integrations details page.

### Checklist

- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Cristina Amico 2023-04-18 08:44:16 +02:00 committed by GitHub
parent f892faceb4
commit 970de9147e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 140 additions and 36 deletions

View file

@ -22,9 +22,6 @@ export const FLEET_CLOUD_SECURITY_POSTURE_KSPM_POLICY_TEMPLATE = 'kspm';
export const PACKAGE_TEMPLATE_SUFFIX = '@package';
export const USER_SETTINGS_TEMPLATE_SUFFIX = '@custom';
export const FLEET_ELASTIC_AGENT_DETAILS_DASHBOARD_ID =
'elastic_agent-f47f18cc-9c7d-4278-b2ea-a6dee816d395';
export const DATASET_VAR_NAME = 'data_stream.dataset';
/*
Package rules:

View file

@ -20,6 +20,7 @@ export * from './fleet_server_policy_config';
export * from './authz';
export * from './file_storage';
export * from './message_signing_keys';
export * from './locators';
// TODO: This is the default `index.max_result_window` ES setting, which dictates
// the maximum amount of results allowed to be returned from a search. It's possible

View file

@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export const LOCATORS_IDS = {
APM_LOCATOR: 'APM_LOCATOR',
DASHBOARD_APP: 'DASHBOARD_APP_LOCATOR',
} as const;
// Dashboards ids
export const DASHBOARD_LOCATORS_IDS = {
ELASTIC_AGENT_OVERVIEW: 'elastic_agent-a148dc70-6b3c-11ed-98de-67bdecd21824',
ELASTIC_AGENT_AGENT_INFO: 'elastic_agent-0600ffa0-6b5e-11ed-98de-67bdecd21824',
ELASTIC_AGENT_AGENT_METRICS: 'elastic_agent-f47f18cc-9c7d-4278-b2ea-a6dee816d395',
ELASTIC_AGENT_INTEGRATIONS: 'elastic_agent-1a4e7280-6b5e-11ed-98de-67bdecd21824',
} as const;

View file

@ -52,6 +52,8 @@ export {
// Statuses
// Authz
ENDPOINT_PRIVILEGES,
// dashboards ids
DASHBOARD_LOCATORS_IDS,
} from './constants';
export {
// Route services

View file

@ -25,6 +25,17 @@ jest.mock('../../../../../../hooks/use_fleet_status', () => ({
jest.mock('../../../../../../hooks/use_request/epm');
jest.mock('../../../../../../hooks/use_locator', () => {
return {
useDashboardLocator: jest.fn().mockImplementation(() => {
return {
id: 'DASHBOARD_APP_LOCATOR',
getRedirectUrl: jest.fn().mockResolvedValue('app/dashboards#/view/elastic_agent-a0001'),
};
}),
};
});
describe('AgentDashboardLink', () => {
it('should enable the button if elastic_agent package is installed and policy has monitoring enabled', async () => {
mockedUseGetPackageInfoByKeyQuery.mockReturnValue({

View file

@ -10,21 +10,26 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { EuiButton, EuiToolTip } from '@elastic/eui';
import styled from 'styled-components';
import { useGetPackageInfoByKeyQuery, useKibanaLink, useLink } from '../../../../hooks';
import { useGetPackageInfoByKeyQuery, useLink, useDashboardLocator } from '../../../../hooks';
import type { Agent, AgentPolicy } from '../../../../types';
import {
FLEET_ELASTIC_AGENT_PACKAGE,
FLEET_ELASTIC_AGENT_DETAILS_DASHBOARD_ID,
DASHBOARD_LOCATORS_IDS,
} from '../../../../../../../common/constants';
function useAgentDashboardLink(agent: Agent) {
const { isLoading, data } = useGetPackageInfoByKeyQuery(FLEET_ELASTIC_AGENT_PACKAGE);
const isInstalled = data?.item.status === 'installed';
const dashboardLocator = useDashboardLocator();
const dashboardLink = useKibanaLink(`/dashboard/${FLEET_ELASTIC_AGENT_DETAILS_DASHBOARD_ID}`);
const query = `_a=(query:(language:kuery,query:'elastic_agent.id:${agent.id}'))`;
const link = `${dashboardLink}?${query}`;
const link = dashboardLocator?.getRedirectUrl({
dashboardId: DASHBOARD_LOCATORS_IDS.ELASTIC_AGENT_AGENT_METRICS,
query: {
language: 'kuery',
query: `elastic_agent.id:${agent.id}`,
},
});
return {
isLoading,
@ -50,7 +55,12 @@ export const AgentDashboardLink: React.FunctionComponent<{
!isInstalled || isLoading || !isLogAndMetricsEnabled ? { disabled: true } : { href: link };
const button = (
<EuiButtonCompressed {...buttonArgs} isLoading={isLoading} color="primary">
<EuiButtonCompressed
{...buttonArgs}
isLoading={isLoading}
color="primary"
iconType="dashboardApp"
>
<FormattedMessage
data-test-subj="agentDetails.viewMoreMetricsButton"
id="xpack.fleet.agentDetails.viewDashboardButtonLabel"

View file

@ -0,0 +1,53 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { DASHBOARD_LOCATORS_IDS } from '../../../../../../../common/constants';
import { useDashboardLocator } from '../../../../hooks';
export const DashboardsButtons: React.FunctionComponent = () => {
const dashboardLocator = useDashboardLocator();
const getDashboardHref = (dashboardId: string) => {
return dashboardLocator?.getRedirectUrl({ dashboardId }) || '';
};
return (
<>
<EuiFlexGroup gutterSize="s" justifyContent="flexStart">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
iconType="dashboardApp"
href={getDashboardHref(DASHBOARD_LOCATORS_IDS.ELASTIC_AGENT_OVERVIEW)}
data-test-subj="ingestOverviewLinkButton"
>
<FormattedMessage
id="xpack.fleet.agentList.ingestOverviewlinkButton"
defaultMessage="Ingest Overview Metrics"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
iconType="dashboardApp"
href={getDashboardHref(DASHBOARD_LOCATORS_IDS.ELASTIC_AGENT_AGENT_INFO)}
data-test-subj="agentInfoLinkButton"
>
<FormattedMessage
id="xpack.fleet.agentList.agentInfoLinkButton"
defaultMessage="Agent Info Metrics"
/>
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
};

View file

@ -36,6 +36,17 @@ jest.mock('../../../../components', () => {
};
});
jest.mock('../../../../../../hooks/use_locator', () => {
return {
useDashboardLocator: jest.fn().mockImplementation(() => {
return {
id: 'DASHBOARD_APP_LOCATOR',
getRedirectUrl: jest.fn().mockResolvedValue('app/dashboards#/view/elastic_agent-a0002'),
};
}),
};
});
const TestComponent = (props: any) => (
<KibanaContextProvider services={coreMock.createStart()}>
<ConfigContext.Provider value={{ agents: { enabled: true, elasticsearch: {} }, enabled: true }}>

View file

@ -31,15 +31,12 @@ import { AgentBulkActions } from './bulk_actions';
import type { SelectionMode } from './types';
import { AgentActivityButton } from './agent_activity_button';
import { AgentStatusFilter } from './agent_status_filter';
import { DashboardsButtons } from './dashboards_buttons';
const ClearAllTagsFilterItem = styled(EuiFilterSelectItem)`
padding: ${(props) => props.theme.eui.euiSizeS};
`;
const FlexEndEuiFlexItem = styled(EuiFlexItem)`
align-self: flex-end;
`;
export const SearchAndFilterBar: React.FunctionComponent<{
agentPolicies: AgentPolicy[];
draftKuery: string;
@ -118,17 +115,18 @@ export const SearchAndFilterBar: React.FunctionComponent<{
return (
<>
{/* Search and filter bar */}
<EuiFlexGroup direction="column">
<FlexEndEuiFlexItem>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem>
{/* Top Buttons and Links */}
<EuiFlexGroup>
<EuiFlexItem>{totalAgents > 0 && <DashboardsButtons />}</EuiFlexItem>
<EuiFlexGroup gutterSize="s" justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<AgentActivityButton
onClickAgentActivity={onClickAgentActivity}
showAgentActivityTour={showAgentActivityTour}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip
content={
<FormattedMessage
@ -145,7 +143,7 @@ export const SearchAndFilterBar: React.FunctionComponent<{
</EuiButton>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip
content={
<FormattedMessage
@ -163,7 +161,8 @@ export const SearchAndFilterBar: React.FunctionComponent<{
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
</FlexEndEuiFlexItem>
</EuiFlexGroup>
{/* Search and filters */}
<EuiFlexItem grow={4}>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={6}>

View file

@ -10,13 +10,15 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import type { DataStream } from '../../../../types';
import { useKibanaLink } from '../../../../hooks';
import { useDashboardLocator } from '../../../../hooks';
import { ContextMenuActions } from '../../../../components';
import { useAPMServiceDetailHref } from '../../../../hooks/use_apm_service_href';
export const DataStreamRowActions = memo<{ datastream: DataStream }>(({ datastream }) => {
const { dashboards } = datastream;
const dashboardLocator = useDashboardLocator();
const actionNameSingular = (
<FormattedMessage
id="xpack.fleet.dataStreamList.viewDashboardActionText"
@ -82,8 +84,7 @@ export const DataStreamRowActions = memo<{ datastream: DataStream }>(({ datastre
items: [
{
icon: 'dashboardApp',
/* eslint-disable-next-line react-hooks/rules-of-hooks */
href: useKibanaLink(`/dashboard/${dashboards[0].id || ''}`),
href: dashboardLocator?.getRedirectUrl({ dashboardId: dashboards[0]?.id } || ''),
name: actionNameSingular,
},
],
@ -109,8 +110,7 @@ export const DataStreamRowActions = memo<{ datastream: DataStream }>(({ datastre
items: dashboards.map((dashboard) => {
return {
icon: 'dashboardApp',
/* eslint-disable-next-line react-hooks/rules-of-hooks */
href: useKibanaLink(`/dashboard/${dashboard.id || ''}`),
href: dashboardLocator?.getRedirectUrl({ dashboardId: dashboard?.id } || ''),
name: dashboard.title,
};
}),

View file

@ -22,6 +22,7 @@ export {
AUTO_UPDATE_PACKAGES,
KEEP_POLICIES_UP_TO_DATE_PACKAGES,
AUTO_UPGRADE_POLICIES_PACKAGES,
LOCATORS_IDS,
} from '../../common/constants';
export * from './page_paths';
@ -37,7 +38,3 @@ export const DURATION_APM_SETTINGS_VARS = {
TAIL_SAMPLING_INTERVAL: 'tail_sampling_interval',
WRITE_TIMEOUT: 'write_timeout',
};
export const LOCATORS_IDS = {
APM_LOCATOR: 'APM_LOCATOR',
} as const;

View file

@ -31,3 +31,4 @@ export * from './use_flyout_context';
export * from './use_is_guided_onboarding_active';
export * from './use_fleet_server_hosts_for_policy';
export * from './use_fleet_server_standalone';
export * from './use_locator';

View file

@ -7,7 +7,7 @@
import type { SerializableRecord } from '@kbn/utility-types';
import type { ValuesType } from 'utility-types';
import type { LOCATORS_IDS } from '../constants';
import { LOCATORS_IDS } from '../constants';
import { useStartServices } from './use_core';
@ -17,3 +17,7 @@ export function useLocator<T extends SerializableRecord>(
const services = useStartServices();
return services.share.url.locators.get<T>(locatorId);
}
export function useDashboardLocator() {
return useLocator(LOCATORS_IDS.DASHBOARD_APP);
}

View file

@ -6,10 +6,9 @@
*/
import expect from '@kbn/expect';
import {
FLEET_ELASTIC_AGENT_PACKAGE,
FLEET_ELASTIC_AGENT_DETAILS_DASHBOARD_ID,
} from '@kbn/fleet-plugin/common/constants/epm';
import { FLEET_ELASTIC_AGENT_PACKAGE } from '@kbn/fleet-plugin/common/constants/epm';
import { DASHBOARD_LOCATORS_IDS } from '@kbn/fleet-plugin/common';
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
import { skipIfNoDockerRegistry } from '../../helpers';
import { setupFleetAndAgents } from '../agents/services';
@ -47,10 +46,10 @@ export default function (providerContext: FtrProviderContext) {
it('Install elastic agent details dashboard with the correct id', async () => {
const resDashboard = await kibanaServer.savedObjects.get({
type: 'dashboard',
id: FLEET_ELASTIC_AGENT_DETAILS_DASHBOARD_ID,
id: DASHBOARD_LOCATORS_IDS.ELASTIC_AGENT_AGENT_METRICS,
});
expect(resDashboard.id).to.eql(FLEET_ELASTIC_AGENT_DETAILS_DASHBOARD_ID);
expect(resDashboard.id).to.eql(DASHBOARD_LOCATORS_IDS.ELASTIC_AGENT_AGENT_METRICS);
});
after(async () => {