Skip to content

Commit

Permalink
[Fleet] Agent details page UI (elastic#64983)
Browse files Browse the repository at this point in the history
* Fix empty host name column in agent list

* Fix empty version column in agent list

* Consolidate page header styling inconsistencies

* Add tabs to agent details

* Add right-side header content and actions menu

* Give headers more spacing when there are tabs present

* Add details tab

* Use ECS formatted metadata

* Make activity log table pretty

* Return agent event SO id from list API

* Fix i18n

* Add types for new agent events and differentiate from stored agent events

* Adjust test

Co-authored-by: Elastic Machine <[email protected]>
  • Loading branch information
jen-huang and elasticmachine committed May 4, 2020
1 parent 90a4a5e commit 6d78606
Show file tree
Hide file tree
Showing 26 changed files with 694 additions and 351 deletions.
8 changes: 6 additions & 2 deletions x-pack/plugins/ingest_manager/common/types/models/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export interface AgentActionSOAttributes extends SavedObjectAttributes {
data?: string;
}

export interface AgentEvent {
export interface NewAgentEvent {
type: 'STATE' | 'ERROR' | 'ACTION_RESULT' | 'ACTION';
subtype: // State
| 'RUNNING'
Expand All @@ -58,7 +58,11 @@ export interface AgentEvent {
stream_id?: string;
}

export interface AgentEventSOAttributes extends AgentEvent, SavedObjectAttributes {}
export interface AgentEvent extends NewAgentEvent {
id: string;
}

export interface AgentEventSOAttributes extends NewAgentEvent, SavedObjectAttributes {}

type MetadataValue = string | AgentMetadata;

Expand Down
12 changes: 10 additions & 2 deletions x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { Agent, AgentAction, AgentEvent, AgentStatus, AgentType, NewAgentAction } from '../models';
import {
Agent,
AgentAction,
NewAgentEvent,
AgentEvent,
AgentStatus,
AgentType,
NewAgentAction,
} from '../models';

export interface GetAgentsRequest {
query: {
Expand Down Expand Up @@ -40,7 +48,7 @@ export interface PostAgentCheckinRequest {
};
body: {
local_metadata?: Record<string, any>;
events?: AgentEvent[];
events?: NewAgentEvent[];
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export const Header: React.FC<HeaderProps> = ({
<EuiFlexGroup>
{tabs ? (
<EuiFlexItem>
<EuiSpacer size="s" />
<Tabs>
{tabs.map(props => (
<EuiTab {...props} key={props.id}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
*/
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
import { EuiLoadingSpinnerSize } from '@elastic/eui/src/components/loading/loading_spinner';

export const Loading: React.FunctionComponent<{}> = () => (
export const Loading: React.FunctionComponent<{ size?: EuiLoadingSpinnerSize }> = ({ size }) => (
<EuiFlexGroup justifyContent="spaceAround">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="xl" />
<EuiLoadingSpinner size={size || 'xl'} />
</EuiFlexItem>
</EuiFlexGroup>
);
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import React, { useState, useEffect } from 'react';
import { IFieldType } from 'src/plugins/data/public';
// @ts-ignore
import { EuiSuggest, EuiSuggestItemProps } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useDebounce, useStartDeps } from '../hooks';
import { INDEX_NAME, AGENT_SAVED_OBJECT_TYPE } from '../constants';

Expand All @@ -30,9 +31,15 @@ interface Props {
value: string;
fieldPrefix: string;
onChange: (newValue: string) => void;
placeholder?: string;
}

export const SearchBar: React.FunctionComponent<Props> = ({ value, fieldPrefix, onChange }) => {
export const SearchBar: React.FunctionComponent<Props> = ({
value,
fieldPrefix,
onChange,
placeholder,
}) => {
const { suggestions } = useSuggestions(fieldPrefix, value);

// TODO fix type when correctly typed in EUI
Expand All @@ -52,7 +59,12 @@ export const SearchBar: React.FunctionComponent<Props> = ({ value, fieldPrefix,
// @ts-ignore
value={value}
icon={'search'}
placeholder={'Search'}
placeholder={
placeholder ||
i18n.translate('xpack.ingestManager.defaultSearchPlaceholderText', {
defaultMessage: 'Search',
})
}
onInputChange={onChangeSearch}
onItemClick={onAutocompleteClick}
suggestions={suggestions.map(suggestion => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const CreateDatasourcePageLayout: React.FunctionComponent<{
const leftColumn = (
<EuiFlexGroup direction="column" gutterSize="s" alignItems="flexStart">
<EuiFlexItem>
<EuiButtonEmpty size="s" iconType="arrowLeft" flush="left" href={cancelUrl}>
<EuiButtonEmpty size="xs" iconType="arrowLeft" flush="left" href={cancelUrl}>
<FormattedMessage
id="xpack.ingestManager.createDatasource.cancelLinkText"
defaultMessage="Cancel"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* 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, { Fragment, memo, useMemo, useState } from 'react';
import React, { memo, useMemo, useState } from 'react';
import { Redirect, useRouteMatch, Switch, Route } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { FormattedMessage, FormattedDate } from '@kbn/i18n/react';
Expand All @@ -13,7 +13,6 @@ import {
EuiCallOut,
EuiText,
EuiSpacer,
EuiTitle,
EuiButtonEmpty,
EuiI18nNumber,
EuiDescriptionList,
Expand Down Expand Up @@ -72,46 +71,40 @@ export const AgentConfigDetailsLayout: React.FunctionComponent = () => {

const headerLeftContent = useMemo(
() => (
<React.Fragment>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<div>
<EuiButtonEmpty iconType="arrowLeft" href={configListLink} flush="left" size="xs">
<FormattedMessage
id="xpack.ingestManager.configDetails.viewAgentListTitle"
defaultMessage="View all agent configurations"
/>
</EuiButtonEmpty>
</div>
<EuiTitle size="l">
<h1>
{(agentConfig && agentConfig.name) || (
<FormattedMessage
id="xpack.ingestManager.configDetails.configDetailsTitle"
defaultMessage="Config '{id}'"
values={{
id: configId,
}}
/>
)}
</h1>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
{agentConfig && agentConfig.description ? (
<Fragment>
<EuiSpacer size="s" />
<EuiText color="subdued" size="s">
{agentConfig.description}
</EuiText>
</Fragment>
) : null}
<EuiFlexGroup direction="column" gutterSize="s" alignItems="flexStart">
<EuiFlexItem>
<EuiButtonEmpty iconType="arrowLeft" href={configListLink} flush="left" size="xs">
<FormattedMessage
id="xpack.ingestManager.configDetails.viewAgentListTitle"
defaultMessage="View all agent configurations"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem>
<EuiText>
<h1>
{(agentConfig && agentConfig.name) || (
<FormattedMessage
id="xpack.ingestManager.configDetails.configDetailsTitle"
defaultMessage="Config '{id}'"
values={{
id: configId,
}}
/>
)}
</h1>
</EuiText>
</EuiFlexItem>

{agentConfig && agentConfig.description ? (
<EuiFlexItem>
<EuiSpacer size="s" />
<EuiText color="subdued" size="s">
{agentConfig.description}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="l" />
</React.Fragment>
) : null}
</EuiFlexGroup>
),
[configListLink, agentConfig, configId]
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const HeroImage = memo(() => {
? toAssets('illustration_integrations_darkmode.svg')
: toAssets('illustration_integrations_lightmode.svg'),
}))`
margin-bottom: -60px;
margin-bottom: -68px;
width: 80%;
`;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* 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, useState, useCallback } from 'react';
import { EuiButton, EuiPopover, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { Agent } from '../../../../types';
import { useCapabilities } from '../../../../hooks';
import { useAgentRefresh } from '../hooks';
import { AgentUnenrollProvider, AgentReassignConfigFlyout } from '../../components';

export const AgentDetailsActionMenu: React.FunctionComponent<{
agent: Agent;
}> = memo(({ agent }) => {
const hasWriteCapabilites = useCapabilities().write;
const refreshAgent = useAgentRefresh();
const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState<boolean>(false);
const handleCloseMenu = useCallback(() => setIsActionsPopoverOpen(false), [
setIsActionsPopoverOpen,
]);
const handleToggleMenu = useCallback(() => setIsActionsPopoverOpen(!isActionsPopoverOpen), [
isActionsPopoverOpen,
]);
const [isReassignFlyoutOpen, setIsReassignFlyoutOpen] = useState(false);

return (
<>
{isReassignFlyoutOpen && (
<AgentReassignConfigFlyout agent={agent} onClose={() => setIsReassignFlyoutOpen(false)} />
)}
<EuiPopover
anchorPosition="downRight"
panelPaddingSize="none"
button={
<EuiButton onClick={handleToggleMenu} iconType="arrowDown" iconSide="right">
<FormattedMessage
id="xpack.ingestManager.agentDetails.actionsButton"
defaultMessage="Actions"
/>
</EuiButton>
}
isOpen={isActionsPopoverOpen}
closePopover={handleCloseMenu}
>
<EuiContextMenuPanel
items={[
<EuiContextMenuItem
icon="pencil"
onClick={() => {
handleCloseMenu();
setIsReassignFlyoutOpen(true);
}}
key="reassignConfig"
>
<FormattedMessage
id="xpack.ingestManager.agentList.reassignActionText"
defaultMessage="Assign new agent config"
/>
</EuiContextMenuItem>,
<AgentUnenrollProvider key="unenrollAgent">
{unenrollAgentsPrompt => (
<EuiContextMenuItem
icon="cross"
disabled={!hasWriteCapabilites || !agent.active}
onClick={() => {
unenrollAgentsPrompt([agent.id], 1, refreshAgent);
}}
>
<FormattedMessage
id="xpack.ingestManager.agentList.unenrollOneButton"
defaultMessage="Unenroll"
/>
</EuiContextMenuItem>
)}
</AgentUnenrollProvider>,
]}
/>
</EuiPopover>
</>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* 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 {
EuiDescriptionList,
EuiDescriptionListTitle,
EuiDescriptionListDescription,
EuiFlexGroup,
EuiFlexItem,
EuiLink,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Agent, AgentConfig } from '../../../../types';
import { AGENT_CONFIG_DETAILS_PATH } from '../../../../constants';
import { useLink } from '../../../../hooks';
import { AgentHealth } from '../../components';

export const AgentDetailsContent: React.FunctionComponent<{
agent: Agent;
agentConfig?: AgentConfig;
}> = memo(({ agent, agentConfig }) => {
const agentConfigUrl = useLink(AGENT_CONFIG_DETAILS_PATH);
return (
<EuiDescriptionList>
{[
{
title: i18n.translate('xpack.ingestManager.agentDetails.hostNameLabel', {
defaultMessage: 'Host name',
}),
description: agent.local_metadata['host.hostname'],
},
{
title: i18n.translate('xpack.ingestManager.agentDetails.hostIdLabel', {
defaultMessage: 'Host ID',
}),
description: agent.id,
},
{
title: i18n.translate('xpack.ingestManager.agentDetails.statusLabel', {
defaultMessage: 'Status',
}),
description: <AgentHealth agent={agent} />,
},
{
title: i18n.translate('xpack.ingestManager.agentDetails.agentConfigurationLabel', {
defaultMessage: 'Agent configuration',
}),
description: agentConfig ? (
<EuiLink href={`${agentConfigUrl}${agent.config_id}`}>
{agentConfig.name || agent.config_id}
</EuiLink>
) : (
agent.config_id || '-'
),
},
{
title: i18n.translate('xpack.ingestManager.agentDetails.versionLabel', {
defaultMessage: 'Agent version',
}),
description: agent.local_metadata['agent.version'],
},
{
title: i18n.translate('xpack.ingestManager.agentDetails.platformLabel', {
defaultMessage: 'Platform',
}),
description: agent.local_metadata['os.platform'],
},
].map(({ title, description }) => {
return (
<EuiFlexGroup>
<EuiFlexItem grow={3}>
<EuiDescriptionListTitle>{title}</EuiDescriptionListTitle>
</EuiFlexItem>
<EuiFlexItem grow={7}>
<EuiDescriptionListDescription>{description}</EuiDescriptionListDescription>
</EuiFlexItem>
</EuiFlexGroup>
);
})}
</EuiDescriptionList>
);
});
Loading

0 comments on commit 6d78606

Please sign in to comment.