[Fleet] Add a link from agent details page to agent dashboard (#127882)

This commit is contained in:
Nicolas Chaulet 2022-03-16 17:43:16 -04:00 committed by GitHub
parent 854da93770
commit 90d3af549e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 287 additions and 146 deletions

View file

@ -21,6 +21,8 @@ export const KUBERNETES_RUN_INSTRUCTIONS =
export const STANDALONE_RUN_INSTRUCTIONS_LINUXMAC = 'sudo ./elastic-agent install';
export const STANDALONE_RUN_INSTRUCTIONS_WINDOWS = '.\\elastic-agent.exe install';
export const FLEET_ELASTIC_AGENT_DETAILS_DASHBOARD_ID =
'elastic_agent-f47f18cc-9c7d-4278-b2ea-a6dee816d395';
/*
Package rules:
| | autoUpdatePackages |

View file

@ -33,11 +33,15 @@ export const epmRouteService = {
return EPM_API_ROUTES.LIMITED_LIST_PATTERN;
},
getInfoPath: (pkgName: string, pkgVersion: string) => {
return EPM_API_ROUTES.INFO_PATTERN.replace('{pkgName}', pkgName).replace(
'{pkgVersion}',
pkgVersion
);
getInfoPath: (pkgName: string, pkgVersion?: string) => {
if (pkgVersion) {
return EPM_API_ROUTES.INFO_PATTERN.replace('{pkgName}', pkgName).replace(
'{pkgVersion}',
pkgVersion
);
} else {
return EPM_API_ROUTES.INFO_PATTERN.replace('{pkgName}', pkgName).replace('/{pkgVersion}', '');
}
},
getStatsPath: (pkgName: string) => {

View file

@ -0,0 +1,79 @@
/*
* 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 { createFleetTestRendererMock } from '../../../../../../mock';
import type { Agent } from '../../../../types';
import { useGetPackageInfoByKey } from '../../../../../../hooks/use_request/epm';
import { AgentDashboardLink } from './agent_dashboard_link';
const mockedUseGetPackageInfoByKey = useGetPackageInfoByKey as jest.MockedFunction<
typeof useGetPackageInfoByKey
>;
jest.mock('../../../../../../hooks/use_fleet_status', () => ({
FleetStatusProvider: (props: any) => {
return props.children;
},
}));
jest.mock('../../../../../../hooks/use_request/epm');
describe('AgentDashboardLink', () => {
it('should enable the button if elastic_agent package is installed', async () => {
mockedUseGetPackageInfoByKey.mockReturnValue({
isLoading: false,
data: {
item: {
status: 'installed',
},
},
} as ReturnType<typeof useGetPackageInfoByKey>);
const testRenderer = createFleetTestRendererMock();
const result = testRenderer.render(
<AgentDashboardLink
agent={
{
id: 'agent-id-123',
} as unknown as Agent
}
/>
);
expect(result.queryByRole('link')).not.toBeNull();
expect(result.getByRole('link').hasAttribute('href')).toBeTruthy();
});
it('should not enable the button if elastic_agent package is installed', async () => {
mockedUseGetPackageInfoByKey.mockReturnValue({
isLoading: false,
data: {
item: {
status: 'not_installed',
},
},
} as ReturnType<typeof useGetPackageInfoByKey>);
const testRenderer = createFleetTestRendererMock();
const result = testRenderer.render(
<AgentDashboardLink
agent={
{
id: 'agent-id-123',
} as unknown as Agent
}
/>
);
expect(result.queryByRole('link')).toBeNull();
expect(result.queryByRole('button')).not.toBeNull();
expect(result.getByRole('button').hasAttribute('disabled')).toBeTruthy();
});
});

View file

@ -0,0 +1,67 @@
/*
* 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 { FormattedMessage } from '@kbn/i18n-react';
import { EuiButton, EuiToolTip } from '@elastic/eui';
import { useGetPackageInfoByKey, useKibanaLink } from '../../../../hooks';
import type { Agent } from '../../../../types';
import {
FLEET_ELASTIC_AGENT_PACKAGE,
FLEET_ELASTIC_AGENT_DETAILS_DASHBOARD_ID,
} from '../../../../../../../common';
function useAgentDashboardLink(agent: Agent) {
const { isLoading, data } = useGetPackageInfoByKey(FLEET_ELASTIC_AGENT_PACKAGE);
const isInstalled = data?.item.status === 'installed';
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}`;
return {
isLoading,
isInstalled,
link,
};
}
export const AgentDashboardLink: React.FunctionComponent<{
agent: Agent;
}> = ({ agent }) => {
const { isInstalled, link, isLoading } = useAgentDashboardLink(agent);
const buttonArgs = !isInstalled || isLoading ? { disabled: true } : { href: link };
const button = (
<EuiButton fill {...buttonArgs} isLoading={isLoading}>
<FormattedMessage
id="xpack.fleet.agentDetails.viewDashboardButtonLabel"
defaultMessage="View agent dashboard"
/>
</EuiButton>
);
if (!isInstalled) {
return (
<EuiToolTip
content={
<FormattedMessage
id="xpack.fleet.agentDetails.viewDashboardButtonDisabledTooltip"
defaultMessage="Agent dashboard not found, you need to install the elastic_agent package."
/>
}
>
{button}
</EuiToolTip>
);
}
return button;
};

View file

@ -18,12 +18,13 @@ import {
EuiToolTip,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { FormattedMessage, FormattedRelative } from '@kbn/i18n-react';
import type { Agent, AgentPolicy } from '../../../../../types';
import { useKibanaVersion } from '../../../../../hooks';
import { isAgentUpgradeable } from '../../../../../services';
import { AgentPolicyPackageBadges, AgentPolicySummaryLine } from '../../../../../components';
import { AgentPolicySummaryLine } from '../../../../../components';
import { AgentHealth } from '../../../components';
// Allows child text to be truncated
const FlexItemWithMinWidth = styled(EuiFlexItem)`
@ -40,6 +41,22 @@ export const AgentDetailsOverviewSection: React.FunctionComponent<{
<EuiPanel>
<EuiDescriptionList compressed>
{[
{
title: i18n.translate('xpack.fleet.agentDetails.statusLabel', {
defaultMessage: 'Status',
}),
description: <AgentHealth agent={agent} />,
},
{
title: i18n.translate('xpack.fleet.agentDetails.lastActivityLabel', {
defaultMessage: 'Last activity',
}),
description: agent.last_checkin ? (
<FormattedRelative value={new Date(agent.last_checkin)} />
) : (
'-'
),
},
{
title: i18n.translate('xpack.fleet.agentDetails.hostIdLabel', {
defaultMessage: 'Agent ID',
@ -83,14 +100,6 @@ export const AgentDetailsOverviewSection: React.FunctionComponent<{
'-'
),
},
{
title: i18n.translate('xpack.fleet.agentDetails.integrationsLabel', {
defaultMessage: 'Integrations',
}),
description: agent.policy_id ? (
<AgentPolicyPackageBadges agentPolicyId={agent.policy_id} hideTitle />
) : null,
},
{
title: i18n.translate('xpack.fleet.agentDetails.hostNameLabel', {
defaultMessage: 'Host name',

View file

@ -8,3 +8,4 @@
export { AgentLogs } from './agent_logs';
export { AgentDetailsActionMenu } from './actions_menu';
export { AgentDetailsContent } from './agent_details';
export { AgentDashboardLink } from './agent_dashboard_link';

View file

@ -7,20 +7,10 @@
import React, { useMemo, useCallback } from 'react';
import { useRouteMatch, Switch, Route, useLocation } from 'react-router-dom';
import {
EuiFlexGroup,
EuiFlexItem,
EuiButtonEmpty,
EuiText,
EuiLink,
EuiDescriptionList,
EuiDescriptionListTitle,
EuiDescriptionListDescription,
} from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiText, EuiSpacer } from '@elastic/eui';
import type { Props as EuiTabProps } from '@elastic/eui/src/components/tabs/tab';
import { FormattedMessage, FormattedRelative } from '@kbn/i18n-react';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { EuiIconTip } from '@elastic/eui';
import type { Agent, AgentPolicy, AgentDetailsReassignPolicyAction } from '../../../types';
import { FLEET_ROUTING_PATHS } from '../../../constants';
@ -31,22 +21,23 @@ import {
useLink,
useBreadcrumbs,
useStartServices,
useKibanaVersion,
useIntraAppState,
} from '../../../hooks';
import { WithHeaderLayout } from '../../../layouts';
import { AgentHealth } from '../components';
import { isAgentUpgradeable } from '../../../services';
import { AgentRefreshContext } from './hooks';
import { AgentLogs, AgentDetailsActionMenu, AgentDetailsContent } from './components';
import {
AgentLogs,
AgentDetailsActionMenu,
AgentDetailsContent,
AgentDashboardLink,
} from './components';
export const AgentDetailsPage: React.FunctionComponent = () => {
const {
params: { agentId, tabId = '' },
} = useRouteMatch<{ agentId: string; tabId?: string }>();
const { getHref } = useLink();
const kibanaVersion = useKibanaVersion();
const {
isLoading,
isInitialRequest,
@ -115,107 +106,28 @@ export const AgentDetailsPage: React.FunctionComponent = () => {
const headerRightContent = useMemo(
() =>
agentData && agentData.item ? (
<EuiFlexGroup justifyContent={'spaceBetween'} direction="row">
{[
{
label: i18n.translate('xpack.fleet.agentDetails.statusLabel', {
defaultMessage: 'Status',
}),
content: <AgentHealth agent={agentData.item} />,
},
{
label: i18n.translate('xpack.fleet.agentDetails.lastActivityLabel', {
defaultMessage: 'Last activity',
}),
content: agentData.item.last_checkin ? (
<FormattedRelative value={new Date(agentData.item.last_checkin)} />
) : (
'-'
),
},
{
label: i18n.translate('xpack.fleet.agentDetails.policyLabel', {
defaultMessage: 'Policy',
}),
content: isAgentPolicyLoading ? (
<Loading size="m" />
) : agentPolicyData?.item ? (
<EuiLink
href={getHref('policy_details', { policyId: agentData.item.policy_id! })}
className="eui-textBreakWord"
>
{agentPolicyData.item.name || agentData.item.policy_id}
</EuiLink>
) : (
agentData.item.policy_id || '-'
),
},
{
label: i18n.translate('xpack.fleet.agentDetails.agentVersionLabel', {
defaultMessage: 'Agent version',
}),
content:
typeof agentData.item.local_metadata.elastic === 'object' &&
typeof agentData.item.local_metadata.elastic.agent === 'object' &&
typeof agentData.item.local_metadata.elastic.agent.version === 'string' ? (
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false} className="eui-textNoWrap">
{agentData.item.local_metadata.elastic.agent.version}
</EuiFlexItem>
{isAgentUpgradeable(agentData.item, kibanaVersion) ? (
<EuiFlexItem grow={false}>
<EuiIconTip
aria-label={i18n.translate(
'xpack.fleet.agentDetails.upgradeAvailableTooltip',
{
defaultMessage: 'Upgrade available',
}
)}
size="m"
type="alert"
color="warning"
content={i18n.translate(
'xpack.fleet.agentDetails.upgradeAvailableTooltip',
{
defaultMessage: 'Upgrade available',
}
)}
/>
</EuiFlexItem>
) : null}
</EuiFlexGroup>
) : (
'-'
),
},
{
content:
isAgentPolicyLoading || agentPolicyData?.item?.is_managed ? undefined : (
<AgentDetailsActionMenu
agent={agentData.item}
agentPolicy={agentPolicyData?.item}
assignFlyoutOpenByDefault={openReassignFlyoutOpenByDefault}
onCancelReassign={
routeState && routeState.onDoneNavigateTo
? reassignCancelClickHandler
: undefined
}
/>
),
},
].map((item, index) => (
<EuiFlexItem grow={false} key={index}>
{item.label ? (
<EuiDescriptionList compressed>
<EuiDescriptionListTitle>{item.label}</EuiDescriptionListTitle>
<EuiDescriptionListDescription>{item.content}</EuiDescriptionListDescription>
</EuiDescriptionList>
) : (
item.content
)}
<>
<EuiSpacer size="m" />
<EuiFlexGroup justifyContent="flexEnd" alignItems="center" gutterSize="s" direction="row">
{!isAgentPolicyLoading && !agentPolicyData?.item?.is_managed && (
<EuiFlexItem grow={false}>
<AgentDetailsActionMenu
agent={agentData.item}
agentPolicy={agentPolicyData?.item}
assignFlyoutOpenByDefault={openReassignFlyoutOpenByDefault}
onCancelReassign={
routeState && routeState.onDoneNavigateTo
? reassignCancelClickHandler
: undefined
}
/>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<AgentDashboardLink agent={agentData?.item} />
</EuiFlexItem>
))}
</EuiFlexGroup>
</EuiFlexGroup>
</>
) : undefined,
/* eslint-disable-next-line react-hooks/exhaustive-deps */
[agentPolicyData, agentData, getHref, isAgentPolicyLoading]

View file

@ -67,7 +67,7 @@ export const useGetLimitedPackages = () => {
});
};
export const useGetPackageInfoByKey = (pkgName: string, pkgVersion: string) => {
export const useGetPackageInfoByKey = (pkgName: string, pkgVersion?: string) => {
return useRequest<GetInfoResponse>({
path: epmRouteService.getInfoPath(pkgName, pkgVersion),
method: 'get',
@ -81,7 +81,7 @@ export const useGetPackageStats = (pkgName: string) => {
});
};
export const sendGetPackageInfoByKey = (pkgName: string, pkgVersion: string) => {
export const sendGetPackageInfoByKey = (pkgName: string, pkgVersion?: string) => {
return sendRequest<GetInfoResponse>({
path: epmRouteService.getInfoPath(pkgName, pkgVersion),
method: 'get',

View file

@ -9852,10 +9852,8 @@
"xpack.fleet.agentDetails.agentNotFoundErrorDescription": "Impossible de trouver l'ID d'agent {agentId}",
"xpack.fleet.agentDetails.agentNotFoundErrorTitle": "Agent introuvable",
"xpack.fleet.agentDetails.agentPolicyLabel": "Politique d'agent",
"xpack.fleet.agentDetails.agentVersionLabel": "Version d'agent",
"xpack.fleet.agentDetails.hostIdLabel": "ID d'agent",
"xpack.fleet.agentDetails.hostNameLabel": "Nom d'hôte",
"xpack.fleet.agentDetails.integrationsLabel": "Intégrations",
"xpack.fleet.agentDetails.integrationsSectionTitle": "Intégrations",
"xpack.fleet.agentDetails.lastActivityLabel": "Dernière activité",
"xpack.fleet.agentDetails.logLevel": "Niveau de logging",
@ -9863,13 +9861,11 @@
"xpack.fleet.agentDetails.monitorMetricsLabel": "Indicateurs de monitoring",
"xpack.fleet.agentDetails.overviewSectionTitle": "Aperçu",
"xpack.fleet.agentDetails.platformLabel": "Plateforme",
"xpack.fleet.agentDetails.policyLabel": "Politique",
"xpack.fleet.agentDetails.releaseLabel": "Version de l'agent",
"xpack.fleet.agentDetails.statusLabel": "Statut",
"xpack.fleet.agentDetails.subTabs.detailsTab": "Détails de l'agent",
"xpack.fleet.agentDetails.subTabs.logsTab": "Logs",
"xpack.fleet.agentDetails.unexceptedErrorTitle": "Erreur lors du chargement de l'agent",
"xpack.fleet.agentDetails.upgradeAvailableTooltip": "Mise à niveau disponible",
"xpack.fleet.agentDetails.versionLabel": "Version d'agent",
"xpack.fleet.agentDetails.viewAgentListTitle": "Afficher tous les agents",
"xpack.fleet.agentDetailsIntegrations.actionsLabel": "Actions",

View file

@ -11686,10 +11686,8 @@
"xpack.fleet.agentDetails.agentNotFoundErrorDescription": "エージェントID {agentId}が見つかりません",
"xpack.fleet.agentDetails.agentNotFoundErrorTitle": "エージェントが見つかりません",
"xpack.fleet.agentDetails.agentPolicyLabel": "エージェントポリシー",
"xpack.fleet.agentDetails.agentVersionLabel": "エージェントバージョン",
"xpack.fleet.agentDetails.hostIdLabel": "エージェントID",
"xpack.fleet.agentDetails.hostNameLabel": "ホスト名",
"xpack.fleet.agentDetails.integrationsLabel": "統合",
"xpack.fleet.agentDetails.integrationsSectionTitle": "統合",
"xpack.fleet.agentDetails.lastActivityLabel": "前回のアクティビティ",
"xpack.fleet.agentDetails.logLevel": "ログレベル",
@ -11697,13 +11695,11 @@
"xpack.fleet.agentDetails.monitorMetricsLabel": "メトリックの監視",
"xpack.fleet.agentDetails.overviewSectionTitle": "概要",
"xpack.fleet.agentDetails.platformLabel": "プラットフォーム",
"xpack.fleet.agentDetails.policyLabel": "ポリシー",
"xpack.fleet.agentDetails.releaseLabel": "エージェントリリース",
"xpack.fleet.agentDetails.statusLabel": "ステータス",
"xpack.fleet.agentDetails.subTabs.detailsTab": "エージェントの詳細",
"xpack.fleet.agentDetails.subTabs.logsTab": "ログ",
"xpack.fleet.agentDetails.unexceptedErrorTitle": "エージェントの読み込み中にエラーが発生しました",
"xpack.fleet.agentDetails.upgradeAvailableTooltip": "アップグレードが利用可能です",
"xpack.fleet.agentDetails.versionLabel": "エージェントバージョン",
"xpack.fleet.agentDetails.viewAgentListTitle": "すべてのエージェントを表示",
"xpack.fleet.agentDetailsIntegrations.actionsLabel": "アクション",

View file

@ -11707,10 +11707,8 @@
"xpack.fleet.agentDetails.agentNotFoundErrorDescription": "找不到代理 ID {agentId}",
"xpack.fleet.agentDetails.agentNotFoundErrorTitle": "未找到代理",
"xpack.fleet.agentDetails.agentPolicyLabel": "代理策略",
"xpack.fleet.agentDetails.agentVersionLabel": "代理版本",
"xpack.fleet.agentDetails.hostIdLabel": "代理 ID",
"xpack.fleet.agentDetails.hostNameLabel": "主机名",
"xpack.fleet.agentDetails.integrationsLabel": "集成",
"xpack.fleet.agentDetails.integrationsSectionTitle": "集成",
"xpack.fleet.agentDetails.lastActivityLabel": "上次活动",
"xpack.fleet.agentDetails.logLevel": "日志记录级别",
@ -11718,13 +11716,11 @@
"xpack.fleet.agentDetails.monitorMetricsLabel": "监测指标",
"xpack.fleet.agentDetails.overviewSectionTitle": "概览",
"xpack.fleet.agentDetails.platformLabel": "平台",
"xpack.fleet.agentDetails.policyLabel": "策略",
"xpack.fleet.agentDetails.releaseLabel": "代理发行版",
"xpack.fleet.agentDetails.statusLabel": "状态",
"xpack.fleet.agentDetails.subTabs.detailsTab": "代理详情",
"xpack.fleet.agentDetails.subTabs.logsTab": "日志",
"xpack.fleet.agentDetails.unexceptedErrorTitle": "加载代理时出错",
"xpack.fleet.agentDetails.upgradeAvailableTooltip": "升级可用",
"xpack.fleet.agentDetails.versionLabel": "代理版本",
"xpack.fleet.agentDetails.viewAgentListTitle": "查看所有代理",
"xpack.fleet.agentDetailsIntegrations.actionsLabel": "操作",

View file

@ -58,5 +58,8 @@ export default function ({ loadTestFile, getService }) {
// Telemetry
loadTestFile(require.resolve('./fleet_telemetry'));
// Integrations
loadTestFile(require.resolve('./integrations'));
});
}

View file

@ -0,0 +1,64 @@
/*
* 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 expect from '@kbn/expect';
import {
FLEET_ELASTIC_AGENT_PACKAGE,
FLEET_ELASTIC_AGENT_DETAILS_DASHBOARD_ID,
} from '../../../../plugins/fleet/common/constants/epm';
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
import { skipIfNoDockerRegistry } from '../../helpers';
import { setupFleetAndAgents } from '../agents/services';
export default function (providerContext: FtrProviderContext) {
describe('Install elastic_agent package', () => {
const { getService } = providerContext;
skipIfNoDockerRegistry(providerContext);
setupFleetAndAgents(providerContext);
const kibanaServer = getService('kibanaServer');
const supertest = getService('supertest');
const dockerServers = getService('dockerServers');
const server = dockerServers.get('registry');
let pkgVersion: string;
before(async () => {
if (!server.enabled) return;
const getPkRes = await supertest
.get(`/api/fleet/epm/packages/${FLEET_ELASTIC_AGENT_PACKAGE}`)
.set('kbn-xsrf', 'xxxx')
.expect(200);
pkgVersion = getPkRes.body.item.version;
// pkgVersion
// Install latest version of the package
await supertest
.post(`/api/fleet/epm/packages/${FLEET_ELASTIC_AGENT_PACKAGE}/${pkgVersion}`)
.send({
force: true,
})
.set('kbn-xsrf', 'xxxx')
.expect(200);
});
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,
});
expect(resDashboard.id).to.eql(FLEET_ELASTIC_AGENT_DETAILS_DASHBOARD_ID);
});
after(async () => {
if (!server.enabled) return;
return supertest
.delete(`/api/fleet/epm/packages/${FLEET_ELASTIC_AGENT_PACKAGE}/${pkgVersion}`)
.set('kbn-xsrf', 'xxxx');
});
});
}

View file

@ -0,0 +1,12 @@
/*
* 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 default function loadTests({ loadTestFile }) {
describe('Integrations', () => {
loadTestFile(require.resolve('./elastic_agent'));
});
}