Skip to content

Commit

Permalink
[Fleet] Add raw status to Agent details UI (#154826)
Browse files Browse the repository at this point in the history
## Summary

Make raw agent status discoverable in Fleet UI, under `Agent details`
tab.

Closes #154067

### Screenshots

<img width="1918" alt="Screenshot 2023-04-19 at 12 14 48"
src="https://user-images.githubusercontent.com/23701614/233059955-7f066ad5-39cd-4685-b76b-41bc31ede4e8.png">
<img width="1918" alt="Screenshot 2023-04-19 at 13 04 06"
src="https://user-images.githubusercontent.com/23701614/233059973-3f1b507f-d0bf-48ab-929b-c567f9814377.png">

### UX checklist

- [ ] Action link title (`View agent JSON`)
- [ ] Flyout title (`{agentName} agent details`)
- [ ] Download button
- [ ] Download button label (`Download JSON`)
- [ ] Downloaded file name (`{agentName}-agent-details.json`)

### Testing steps

1. Run Kibana in dev on this branch.
2. In Fleet, click on an agent to get to the agent details page.
3. There should be a new `View agent JSON` item in the `Actions` menu.
Click it.
4. A new flyout should open with the agent details in JSON format.
Clicking outside of the flyout or on the `Close` button should close the
flyout.
5. The `Download JSON` button should download the JSON correctly.

### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [x] [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
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] 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))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
jillguyonnet and kibanamachine authored Apr 21, 2023
1 parent cd90b11 commit b97b18e
Show file tree
Hide file tree
Showing 3 changed files with 218 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import React, { memo, useState, useMemo } from 'react';
import React, { memo, useState, useMemo, useCallback } from 'react';
import { EuiPortal, EuiContextMenuItem } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';

Expand All @@ -24,6 +24,8 @@ import { isAgentUpgradeable, policyHasFleetServer } from '../../../../services';
import { AgentRequestDiagnosticsModal } from '../../components/agent_request_diagnostics_modal';
import { ExperimentalFeaturesService } from '../../../../services';

import { AgentDetailsJsonFlyout } from './agent_details_json_flyout';

export const AgentDetailsActionMenu: React.FunctionComponent<{
agent: Agent;
agentPolicy?: AgentPolicy;
Expand All @@ -37,8 +39,17 @@ export const AgentDetailsActionMenu: React.FunctionComponent<{
const [isUnenrollModalOpen, setIsUnenrollModalOpen] = useState(false);
const [isUpgradeModalOpen, setIsUpgradeModalOpen] = useState(false);
const [isRequestDiagnosticsModalOpen, setIsRequestDiagnosticsModalOpen] = useState(false);
const [isAgentDetailsJsonFlyoutOpen, setIsAgentDetailsJsonFlyoutOpen] = useState<boolean>(false);
const isUnenrolling = agent.status === 'unenrolling';

const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
const onContextMenuChange = useCallback(
(open: boolean) => {
setIsContextMenuOpen(open);
},
[setIsContextMenuOpen]
);

const hasFleetServer = agentPolicy && policyHasFleetServer(agentPolicy);
const { diagnosticFileUploadEnabled } = ExperimentalFeaturesService.get();

Expand Down Expand Up @@ -70,6 +81,7 @@ export const AgentDetailsActionMenu: React.FunctionComponent<{
onClick={() => {
setIsUnenrollModalOpen(true);
}}
key="unenrollAgent"
>
{isUnenrolling ? (
<FormattedMessage
Expand All @@ -89,12 +101,26 @@ export const AgentDetailsActionMenu: React.FunctionComponent<{
onClick={() => {
setIsUpgradeModalOpen(true);
}}
key="upgradeAgent"
>
<FormattedMessage
id="xpack.fleet.agentList.upgradeOneButton"
defaultMessage="Upgrade agent"
/>
</EuiContextMenuItem>,
<EuiContextMenuItem
icon="inspect"
onClick={() => {
setIsContextMenuOpen(false);
setIsAgentDetailsJsonFlyoutOpen(!isAgentDetailsJsonFlyoutOpen);
}}
key="agentDetailsJson"
>
<FormattedMessage
id="xpack.fleet.agentList.viewAgentDetailsJsonText"
defaultMessage="View agent JSON"
/>
</EuiContextMenuItem>,
];

if (diagnosticFileUploadEnabled) {
Expand All @@ -105,6 +131,7 @@ export const AgentDetailsActionMenu: React.FunctionComponent<{
onClick={() => {
setIsRequestDiagnosticsModalOpen(true);
}}
key="requestDiagnostics"
>
<FormattedMessage
id="xpack.fleet.agentList.diagnosticsOneButton"
Expand Down Expand Up @@ -158,7 +185,17 @@ export const AgentDetailsActionMenu: React.FunctionComponent<{
/>
</EuiPortal>
)}
{isAgentDetailsJsonFlyoutOpen && (
<EuiPortal>
<AgentDetailsJsonFlyout
agent={agent}
onClose={() => setIsAgentDetailsJsonFlyoutOpen(false)}
/>
</EuiPortal>
)}
<ContextMenuActions
isOpen={isContextMenuOpen}
onChange={onContextMenuChange}
button={{
props: { iconType: 'arrowDown', iconSide: 'right', color: 'primary' },
children: (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* 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 { render } from '@testing-library/react';

import type { Agent } from '../../../../types';
import { useStartServices } from '../../../../hooks';

import { AgentDetailsJsonFlyout } from './agent_details_json_flyout';

jest.mock('../../../../hooks');

const mockUseStartServices = useStartServices as jest.Mock;

describe('AgentDetailsJsonFlyout', () => {
const agent: Agent = {
id: '123',
packages: [],
type: 'PERMANENT',
active: true,
enrolled_at: `${Date.now()}`,
user_provided_metadata: {},
local_metadata: {},
};

beforeEach(() => {
mockUseStartServices.mockReturnValue({
docLinks: { links: { fleet: { troubleshooting: 'https://elastic.co' } } },
});
});

const renderComponent = () => {
return render(<AgentDetailsJsonFlyout agent={agent} onClose={jest.fn()} />);
};

it('renders a title with the agent id if host name is not defined', () => {
const result = renderComponent();
expect(result.getByText("'123' agent details")).toBeInTheDocument();
});

it('renders a title with the agent host name if defined', () => {
agent.local_metadata = {
host: {
hostname: '456',
},
};
const result = renderComponent();
expect(result.getByText("'456' agent details")).toBeInTheDocument();
});

it('does not add a link to the page after clicking Download', () => {
const result = renderComponent();
const downloadButton = result.getByRole('button', { name: 'Download JSON' });
const anchorMocked = {
href: '',
click: jest.fn(),
download: '',
setAttribute: jest.fn(),
} as any;
const createElementSpyOn = jest
.spyOn(document, 'createElement')
.mockReturnValueOnce(anchorMocked);

downloadButton.click();
expect(createElementSpyOn).toBeCalledWith('a');
expect(result.queryAllByRole('link')).toHaveLength(1); // The only link is the one from the flyout's description.
expect(result.getByRole('link')).toHaveAttribute('href', 'https://elastic.co');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* 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, { memo } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiButton,
EuiButtonEmpty,
EuiCodeBlock,
EuiFlexGroup,
EuiFlexItem,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiFlyoutHeader,
EuiLink,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';

import type { Agent } from '../../../../types';
import { useStartServices } from '../../../../hooks';

export const AgentDetailsJsonFlyout = memo<{ agent: Agent; onClose: () => void }>(
({ agent, onClose }) => {
const agentToJson = JSON.stringify(agent, null, 2);
const agentName =
typeof agent.local_metadata?.host?.hostname === 'string'
? agent.local_metadata.host.hostname
: agent.id;

const downloadJson = () => {
const link = document.createElement('a');
link.href = `data:text/json;charset=utf-8,${encodeURIComponent(agentToJson)}`;
link.download = `${agentName}-agent-details.json`;
link.click();
};

const { docLinks } = useStartServices();

return (
<EuiFlyout onClose={onClose} size="l" maxWidth={640}>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<h2>
<FormattedMessage
id="xpack.fleet.agentDetails.jsonFlyoutTitle"
defaultMessage="'{name}' agent details"
values={{
name: agentName,
}}
/>
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiText>
<p>
<FormattedMessage
id="xpack.fleet.agentDetails.jsonFlyoutDescription"
defaultMessage="The JSON below is the raw agent data tracked by Fleet. This data can be useful for debugging or troubleshooting Elastic Agent. For more information, see the {doc}."
values={{
doc: (
<EuiLink href={docLinks.links.fleet.troubleshooting}>
<FormattedMessage
id="xpack.fleet.agentDetails.jsonFlyoutDocLink"
defaultMessage="troubleshooting documentation"
/>
</EuiLink>
),
}}
/>
</p>
</EuiText>
<EuiSpacer />
<EuiCodeBlock language="json">{agentToJson}</EuiCodeBlock>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={onClose} flush="left">
<FormattedMessage
id="xpack.fleet.agentDetails.agentDetailsJsonFlyoutCloseButtonLabel"
defaultMessage="Close"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton iconType="download" onClick={downloadJson}>
<FormattedMessage
id="xpack.fleet.agentDetails.agentDetailsJsonDownloadButtonLabel"
defaultMessage="Download JSON"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
);
}
);

0 comments on commit b97b18e

Please sign in to comment.