[Fleet] Agent details page UI (#64983)
* 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 <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
bda8309864
commit
f9be590d51
|
@ -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'
|
||||
|
@ -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;
|
||||
|
||||
|
|
|
@ -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: {
|
||||
|
@ -40,7 +48,7 @@ export interface PostAgentCheckinRequest {
|
|||
};
|
||||
body: {
|
||||
local_metadata?: Record<string, any>;
|
||||
events?: AgentEvent[];
|
||||
events?: NewAgentEvent[];
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 => {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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';
|
||||
|
@ -13,7 +13,6 @@ import {
|
|||
EuiCallOut,
|
||||
EuiText,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
EuiButtonEmpty,
|
||||
EuiI18nNumber,
|
||||
EuiDescriptionList,
|
||||
|
@ -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]
|
||||
);
|
||||
|
|
|
@ -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%;
|
||||
`;
|
||||
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
});
|
|
@ -13,14 +13,19 @@ import {
|
|||
EuiButton,
|
||||
EuiSpacer,
|
||||
EuiFlexItem,
|
||||
EuiTitle,
|
||||
EuiBadge,
|
||||
EuiText,
|
||||
EuiButtonIcon,
|
||||
EuiCodeBlock,
|
||||
} from '@elastic/eui';
|
||||
import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage, FormattedTime } from '@kbn/i18n/react';
|
||||
import { AGENT_EVENT_SAVED_OBJECT_TYPE } from '../../../../constants';
|
||||
import { Agent, AgentEvent } from '../../../../types';
|
||||
import { usePagination, useGetOneAgentEvents } from '../../../../hooks';
|
||||
import { SearchBar } from '../../../../components/search_bar';
|
||||
import { TYPE_LABEL, SUBTYPE_LABEL } from './type_labels';
|
||||
|
||||
function useSearch() {
|
||||
const [state, setState] = useState<{ search: string }>({
|
||||
|
@ -41,6 +46,9 @@ function useSearch() {
|
|||
export const AgentEventsTable: React.FunctionComponent<{ agent: Agent }> = ({ agent }) => {
|
||||
const { pageSizeOptions, pagination, setPagination } = usePagination();
|
||||
const { search, setSearch } = useSearch();
|
||||
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<{
|
||||
[key: string]: JSX.Element;
|
||||
}>({});
|
||||
|
||||
const { isLoading, data, sendRequest } = useGetOneAgentEvents(agent.id, {
|
||||
page: pagination.currentPage,
|
||||
|
@ -59,6 +67,49 @@ export const AgentEventsTable: React.FunctionComponent<{ agent: Agent }> = ({ ag
|
|||
pageSizeOptions,
|
||||
};
|
||||
|
||||
const toggleDetails = (agentEvent: AgentEvent) => {
|
||||
const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap };
|
||||
if (itemIdToExpandedRowMapValues[agentEvent.id]) {
|
||||
delete itemIdToExpandedRowMapValues[agentEvent.id];
|
||||
} else {
|
||||
const details = (
|
||||
<div style={{ width: '100%' }}>
|
||||
<div>
|
||||
<EuiText size="s">
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.ingestManager.agentEventsList.messageDetailsTitle"
|
||||
defaultMessage="Message"
|
||||
/>
|
||||
</strong>
|
||||
<EuiSpacer size="xs" />
|
||||
<p>{agentEvent.message}</p>
|
||||
</EuiText>
|
||||
</div>
|
||||
{agentEvent.payload ? (
|
||||
<div>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText size="s">
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.ingestManager.agentEventsList.payloadDetailsTitle"
|
||||
defaultMessage="Payload"
|
||||
/>
|
||||
</strong>
|
||||
</EuiText>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiCodeBlock language="json" paddingSize="s" overflowHeight={200}>
|
||||
{JSON.stringify(agentEvent.payload, null, 2)}
|
||||
</EuiCodeBlock>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
itemIdToExpandedRowMapValues[agentEvent.id] = details;
|
||||
}
|
||||
setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
field: 'timestamp',
|
||||
|
@ -66,40 +117,63 @@ export const AgentEventsTable: React.FunctionComponent<{ agent: Agent }> = ({ ag
|
|||
defaultMessage: 'Timestamp',
|
||||
}),
|
||||
render: (timestamp: string) => (
|
||||
<FormattedTime value={new Date(timestamp)} month="numeric" day="numeric" year="numeric" />
|
||||
<FormattedTime
|
||||
value={new Date(timestamp)}
|
||||
month="short"
|
||||
day="numeric"
|
||||
year="numeric"
|
||||
hour="numeric"
|
||||
minute="numeric"
|
||||
second="numeric"
|
||||
/>
|
||||
),
|
||||
sortable: true,
|
||||
width: '18%',
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
name: i18n.translate('xpack.ingestManager.agentEventsList.typeColumnTitle', {
|
||||
defaultMessage: 'Type',
|
||||
}),
|
||||
width: '90px',
|
||||
width: '10%',
|
||||
render: (type: AgentEvent['type']) =>
|
||||
TYPE_LABEL[type] || <EuiBadge color="hollow">{type}</EuiBadge>,
|
||||
},
|
||||
{
|
||||
field: 'subtype',
|
||||
name: i18n.translate('xpack.ingestManager.agentEventsList.subtypeColumnTitle', {
|
||||
defaultMessage: 'Subtype',
|
||||
}),
|
||||
width: '90px',
|
||||
width: '13%',
|
||||
render: (subtype: AgentEvent['subtype']) =>
|
||||
SUBTYPE_LABEL[subtype] || <EuiBadge color="hollow">{subtype}</EuiBadge>,
|
||||
},
|
||||
{
|
||||
field: 'message',
|
||||
name: i18n.translate('xpack.ingestManager.agentEventsList.messageColumnTitle', {
|
||||
defaultMessage: 'Message',
|
||||
}),
|
||||
render: (message: string) => <EuiText size="xs">{message}</EuiText>,
|
||||
truncateText: true,
|
||||
},
|
||||
{
|
||||
field: 'payload',
|
||||
name: i18n.translate('xpack.ingestManager.agentEventsList.paylodColumnTitle', {
|
||||
defaultMessage: 'Payload',
|
||||
}),
|
||||
truncateText: true,
|
||||
render: (payload: any) => (
|
||||
<span>
|
||||
<code>{payload && JSON.stringify(payload, null, 2)}</code>
|
||||
</span>
|
||||
align: RIGHT_ALIGNMENT,
|
||||
width: '40px',
|
||||
isExpander: true,
|
||||
render: (agentEvent: AgentEvent) => (
|
||||
<EuiButtonIcon
|
||||
onClick={() => toggleDetails(agentEvent)}
|
||||
aria-label={
|
||||
itemIdToExpandedRowMap[agentEvent.id]
|
||||
? i18n.translate('xpack.ingestManager.agentEventsList.collapseDetailsAriaLabel', {
|
||||
defaultMessage: 'Hide details',
|
||||
})
|
||||
: i18n.translate('xpack.ingestManager.agentEventsList.expandDetailsAriaLabel', {
|
||||
defaultMessage: 'Show details',
|
||||
})
|
||||
}
|
||||
iconType={itemIdToExpandedRowMap[agentEvent.id] ? 'arrowUp' : 'arrowDown'}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
@ -120,25 +194,20 @@ export const AgentEventsTable: React.FunctionComponent<{ agent: Agent }> = ({ ag
|
|||
|
||||
return (
|
||||
<>
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.ingestManager.agentEventsList.title"
|
||||
defaultMessage="Activity Log"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<SearchBar
|
||||
value={search}
|
||||
onChange={setSearch}
|
||||
fieldPrefix={AGENT_EVENT_SAVED_OBJECT_TYPE}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.ingestManager.agentEventsList.searchPlaceholderText',
|
||||
{ defaultMessage: 'Search for activity logs' }
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={null}>
|
||||
<EuiButton color="secondary" iconType="refresh" onClick={onClickRefresh}>
|
||||
<EuiButton iconType="refresh" onClick={onClickRefresh}>
|
||||
<FormattedMessage
|
||||
id="xpack.ingestManager.agentEventsList.refreshButton"
|
||||
defaultMessage="Refresh"
|
||||
|
@ -150,9 +219,11 @@ export const AgentEventsTable: React.FunctionComponent<{ agent: Agent }> = ({ ag
|
|||
<EuiBasicTable<AgentEvent>
|
||||
onChange={onChange}
|
||||
items={list}
|
||||
itemId="id"
|
||||
columns={columns}
|
||||
pagination={paginationOptions}
|
||||
loading={isLoading}
|
||||
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,206 +0,0 @@
|
|||
/*
|
||||
* 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, { useState, Fragment, useCallback } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
EuiTitle,
|
||||
EuiSpacer,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiDescriptionList,
|
||||
EuiButton,
|
||||
EuiPopover,
|
||||
EuiDescriptionListTitle,
|
||||
EuiDescriptionListDescription,
|
||||
EuiButtonEmpty,
|
||||
EuiIconTip,
|
||||
EuiContextMenuPanel,
|
||||
EuiContextMenuItem,
|
||||
EuiTextColor,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useAgentRefresh } from '../hooks';
|
||||
import { AgentMetadataFlyout } from './metadata_flyout';
|
||||
import { Agent } from '../../../../types';
|
||||
import { AgentHealth } from '../../components/agent_health';
|
||||
import { useCapabilities, useGetOneAgentConfig } from '../../../../hooks';
|
||||
import { Loading } from '../../../../components';
|
||||
import { ConnectedLink, AgentReassignConfigFlyout } from '../../components';
|
||||
import { AgentUnenrollProvider } from '../../components/agent_unenroll_provider';
|
||||
|
||||
const Item: React.FunctionComponent<{ label: string }> = ({ label, children }) => {
|
||||
return (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiDescriptionList compressed>
|
||||
<EuiDescriptionListTitle>{label}</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>{children}</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
||||
|
||||
function useFlyout() {
|
||||
const [isVisible, setVisible] = useState(false);
|
||||
return {
|
||||
isVisible,
|
||||
show: () => setVisible(true),
|
||||
hide: () => setVisible(false),
|
||||
};
|
||||
}
|
||||
|
||||
interface Props {
|
||||
agent: Agent;
|
||||
}
|
||||
export const AgentDetailSection: React.FunctionComponent<Props> = ({ agent }) => {
|
||||
const hasWriteCapabilites = useCapabilities().write;
|
||||
const metadataFlyout = useFlyout();
|
||||
const refreshAgent = useAgentRefresh();
|
||||
// Actions menu
|
||||
const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false);
|
||||
const handleCloseMenu = useCallback(() => setIsActionsPopoverOpen(false), [
|
||||
setIsActionsPopoverOpen,
|
||||
]);
|
||||
const handleToggleMenu = useCallback(() => setIsActionsPopoverOpen(!isActionsPopoverOpen), [
|
||||
isActionsPopoverOpen,
|
||||
]);
|
||||
const [isReassignFlyoutOpen, setIsReassignFlyoutOpen] = useState(false);
|
||||
|
||||
// Fetch AgentConfig information
|
||||
const { isLoading: isAgentConfigLoading, data: agentConfigData } = useGetOneAgentConfig(
|
||||
agent.config_id
|
||||
);
|
||||
|
||||
const items = [
|
||||
{
|
||||
title: i18n.translate('xpack.ingestManager.agentDetails.statusLabel', {
|
||||
defaultMessage: 'Status',
|
||||
}),
|
||||
description: <AgentHealth agent={agent} />,
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.ingestManager.agentDetails.idLabel', {
|
||||
defaultMessage: 'ID',
|
||||
}),
|
||||
description: agent.id,
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.ingestManager.agentDetails.typeLabel', {
|
||||
defaultMessage: 'Type',
|
||||
}),
|
||||
description: agent.type,
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.ingestManager.agentDetails.agentConfigLabel', {
|
||||
defaultMessage: 'AgentConfig',
|
||||
}),
|
||||
description: isAgentConfigLoading ? (
|
||||
<Loading />
|
||||
) : agentConfigData && agentConfigData.item ? (
|
||||
<ConnectedLink color="primary" path={`/configs/${agent.config_id}`}>
|
||||
{agentConfigData.item.name}
|
||||
</ConnectedLink>
|
||||
) : (
|
||||
<Fragment>
|
||||
<EuiIconTip
|
||||
position="bottom"
|
||||
color="primary"
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="xpack.ingestManager.agentDetails.unavailableConfigTooltipText"
|
||||
defaultMessage="This config is no longer available"
|
||||
/>
|
||||
}
|
||||
/>{' '}
|
||||
<EuiTextColor color="subdued">{agent.config_id}</EuiTextColor>
|
||||
</Fragment>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{isReassignFlyoutOpen && (
|
||||
<AgentReassignConfigFlyout agent={agent} onClose={() => setIsReassignFlyoutOpen(false)} />
|
||||
)}
|
||||
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="l">
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.ingestManager.agentDetails.agentDetailsTitle"
|
||||
defaultMessage="Agent detail"
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
anchorPosition="downRight"
|
||||
panelPaddingSize="none"
|
||||
button={
|
||||
<EuiButton onClick={handleToggleMenu}>
|
||||
<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>
|
||||
{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>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size={'xl'} />
|
||||
<EuiFlexGroup alignItems="flexStart" justifyContent="spaceBetween">
|
||||
{items.map((item, idx) => (
|
||||
<Item key={idx} label={item.title}>
|
||||
{item.description}
|
||||
</Item>
|
||||
))}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty onClick={() => metadataFlyout.show()}>View metadata</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{metadataFlyout.isVisible && <AgentMetadataFlyout flyout={metadataFlyout} agent={agent} />}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -4,4 +4,5 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
export { AgentEventsTable } from './agent_events_table';
|
||||
export { AgentDetailSection } from './details_section';
|
||||
export { AgentDetailsActionMenu } from './actions_menu';
|
||||
export { AgentDetailsContent } from './agent_details';
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { EuiBadge } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { AgentEvent } from '../../../../types';
|
||||
|
||||
export const TYPE_LABEL: { [key in AgentEvent['type']]: JSX.Element } = {
|
||||
STATE: (
|
||||
<EuiBadge color="hollow">
|
||||
<FormattedMessage id="xpack.ingestManager.agentEventType.stateLabel" defaultMessage="State" />
|
||||
</EuiBadge>
|
||||
),
|
||||
ERROR: (
|
||||
<EuiBadge color="danger">
|
||||
<FormattedMessage id="xpack.ingestManager.agentEventType.errorLabel" defaultMessage="Error" />
|
||||
</EuiBadge>
|
||||
),
|
||||
ACTION_RESULT: (
|
||||
<EuiBadge color="secondary">
|
||||
<FormattedMessage
|
||||
id="xpack.ingestManager.agentEventType.actionResultLabel"
|
||||
defaultMessage="Action result"
|
||||
/>
|
||||
</EuiBadge>
|
||||
),
|
||||
ACTION: (
|
||||
<EuiBadge color="primary">
|
||||
<FormattedMessage
|
||||
id="xpack.ingestManager.agentEventType.actionLabel"
|
||||
defaultMessage="Action"
|
||||
/>
|
||||
</EuiBadge>
|
||||
),
|
||||
};
|
||||
|
||||
export const SUBTYPE_LABEL: { [key in AgentEvent['subtype']]: JSX.Element } = {
|
||||
RUNNING: (
|
||||
<EuiBadge color="hollow">
|
||||
<FormattedMessage
|
||||
id="xpack.ingestManager.agentEventSubtype.runningLabel"
|
||||
defaultMessage="Running"
|
||||
/>
|
||||
</EuiBadge>
|
||||
),
|
||||
STARTING: (
|
||||
<EuiBadge color="hollow">
|
||||
<FormattedMessage
|
||||
id="xpack.ingestManager.agentEventSubtype.startingLabel"
|
||||
defaultMessage="Starting"
|
||||
/>
|
||||
</EuiBadge>
|
||||
),
|
||||
IN_PROGRESS: (
|
||||
<EuiBadge color="hollow">
|
||||
<FormattedMessage
|
||||
id="xpack.ingestManager.agentEventSubtype.inProgressLabel"
|
||||
defaultMessage="In progress"
|
||||
/>
|
||||
</EuiBadge>
|
||||
),
|
||||
CONFIG: (
|
||||
<EuiBadge color="hollow">
|
||||
<FormattedMessage
|
||||
id="xpack.ingestManager.agentEventSubtype.configLabel"
|
||||
defaultMessage="Config"
|
||||
/>
|
||||
</EuiBadge>
|
||||
),
|
||||
FAILED: (
|
||||
<EuiBadge color="hollow">
|
||||
<FormattedMessage
|
||||
id="xpack.ingestManager.agentEventSubtype.failedLabel"
|
||||
defaultMessage="Failed"
|
||||
/>
|
||||
</EuiBadge>
|
||||
),
|
||||
STOPPING: (
|
||||
<EuiBadge color="hollow">
|
||||
<FormattedMessage
|
||||
id="xpack.ingestManager.agentEventSubtype.stoppingLabel"
|
||||
defaultMessage="Stopping"
|
||||
/>
|
||||
</EuiBadge>
|
||||
),
|
||||
STOPPED: (
|
||||
<EuiBadge color="hollow">
|
||||
<FormattedMessage
|
||||
id="xpack.ingestManager.agentEventSubtype.stoppedLabel"
|
||||
defaultMessage="Stopped"
|
||||
/>
|
||||
</EuiBadge>
|
||||
),
|
||||
DATA_DUMP: (
|
||||
<EuiBadge color="hollow">
|
||||
<FormattedMessage
|
||||
id="xpack.ingestManager.agentEventSubtype.dataDumpLabel"
|
||||
defaultMessage="Data dump"
|
||||
/>
|
||||
</EuiBadge>
|
||||
),
|
||||
ACKNOWLEDGED: (
|
||||
<EuiBadge color="hollow">
|
||||
<FormattedMessage
|
||||
id="xpack.ingestManager.agentEventSubtype.acknowledgedLabel"
|
||||
defaultMessage="Acknowledged"
|
||||
/>
|
||||
</EuiBadge>
|
||||
),
|
||||
UNKNOWN: (
|
||||
<EuiBadge color="hollow">
|
||||
<FormattedMessage
|
||||
id="xpack.ingestManager.agentEventSubtype.unknownLabel"
|
||||
defaultMessage="Unknown"
|
||||
/>
|
||||
</EuiBadge>
|
||||
),
|
||||
};
|
|
@ -3,64 +3,229 @@
|
|||
* 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 from 'react';
|
||||
import { useRouteMatch } from 'react-router-dom';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useRouteMatch, Switch, Route } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonEmpty,
|
||||
EuiText,
|
||||
EuiLink,
|
||||
EuiDescriptionList,
|
||||
EuiDescriptionListTitle,
|
||||
EuiDescriptionListDescription,
|
||||
} from '@elastic/eui';
|
||||
import { Props as EuiTabProps } from '@elastic/eui/src/components/tabs/tab';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiCallOut, EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { AgentEventsTable, AgentDetailSection } from './components';
|
||||
import { AgentRefreshContext } from './hooks';
|
||||
import { Loading } from '../../../components';
|
||||
import { useGetOneAgent } from '../../../hooks';
|
||||
import {
|
||||
FLEET_AGENTS_PATH,
|
||||
FLEET_AGENT_DETAIL_PATH,
|
||||
AGENT_CONFIG_DETAILS_PATH,
|
||||
} from '../../../constants';
|
||||
import { Loading, Error } from '../../../components';
|
||||
import { useGetOneAgent, useGetOneAgentConfig, useLink } from '../../../hooks';
|
||||
import { WithHeaderLayout } from '../../../layouts';
|
||||
import { AgentHealth } from '../components';
|
||||
import { AgentEventsTable, AgentDetailsActionMenu, AgentDetailsContent } from './components';
|
||||
|
||||
const Divider = styled.div`
|
||||
width: 0;
|
||||
height: 100%;
|
||||
border-left: ${props => props.theme.eui.euiBorderThin};
|
||||
`;
|
||||
|
||||
export const AgentDetailsPage: React.FunctionComponent = () => {
|
||||
const {
|
||||
params: { agentId },
|
||||
} = useRouteMatch();
|
||||
const agentRequest = useGetOneAgent(agentId, {
|
||||
params: { agentId, tabId = '' },
|
||||
} = useRouteMatch<{ agentId: string; tabId?: string }>();
|
||||
const {
|
||||
isLoading,
|
||||
isInitialRequest,
|
||||
error,
|
||||
data: agentData,
|
||||
sendRequest: sendAgentRequest,
|
||||
} = useGetOneAgent(agentId, {
|
||||
pollIntervalMs: 5000,
|
||||
});
|
||||
const {
|
||||
isLoading: isAgentConfigLoading,
|
||||
data: agentConfigData,
|
||||
sendRequest: sendAgentConfigRequest,
|
||||
} = useGetOneAgentConfig(agentData?.item?.config_id);
|
||||
|
||||
if (agentRequest.isLoading && agentRequest.isInitialRequest) {
|
||||
return <Loading />;
|
||||
}
|
||||
const agentListUrl = useLink(FLEET_AGENTS_PATH);
|
||||
const agentActivityTabUrl = useLink(`${FLEET_AGENT_DETAIL_PATH}${agentId}/activity`);
|
||||
const agentDetailsTabUrl = useLink(`${FLEET_AGENT_DETAIL_PATH}${agentId}/details`);
|
||||
const agentConfigUrl = useLink(AGENT_CONFIG_DETAILS_PATH);
|
||||
|
||||
if (agentRequest.error) {
|
||||
return (
|
||||
<WithHeaderLayout>
|
||||
<EuiCallOut
|
||||
title={i18n.translate('xpack.ingestManager.agentDetails.unexceptedErrorTitle', {
|
||||
defaultMessage: 'An error happened while loading the agent',
|
||||
})}
|
||||
color="danger"
|
||||
iconType="alert"
|
||||
>
|
||||
<p>
|
||||
<EuiText>{agentRequest.error.message}</EuiText>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
</WithHeaderLayout>
|
||||
);
|
||||
}
|
||||
const headerLeftContent = useMemo(
|
||||
() => (
|
||||
<EuiFlexGroup direction="column" gutterSize="s" alignItems="flexStart">
|
||||
<EuiFlexItem>
|
||||
<EuiButtonEmpty iconType="arrowLeft" href={agentListUrl} flush="left" size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.ingestManager.agentDetails.viewAgentListTitle"
|
||||
defaultMessage="View all agent configurations"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText>
|
||||
<h1>
|
||||
{agentData?.item?.local_metadata['host.hostname'] || (
|
||||
<FormattedMessage
|
||||
id="xpack.ingestManager.agentDetails.agentDetailsTitle"
|
||||
defaultMessage="Agent '{id}'"
|
||||
values={{
|
||||
id: agentId,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</h1>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
[agentData, agentId, agentListUrl]
|
||||
);
|
||||
|
||||
if (!agentRequest.data) {
|
||||
return (
|
||||
<WithHeaderLayout>
|
||||
<FormattedMessage
|
||||
id="xpack.ingestManager.agentDetails.agentNotFoundErrorTitle"
|
||||
defaultMessage="Agent Not found"
|
||||
/>
|
||||
</WithHeaderLayout>
|
||||
);
|
||||
}
|
||||
const headerRightContent = useMemo(
|
||||
() =>
|
||||
agentData && agentData.item ? (
|
||||
<EuiFlexGroup justifyContent={'flexEnd'} direction="row">
|
||||
{[
|
||||
{
|
||||
label: i18n.translate('xpack.ingestManager.agentDetails.statusLabel', {
|
||||
defaultMessage: 'Status',
|
||||
}),
|
||||
content: <AgentHealth agent={agentData.item} />,
|
||||
},
|
||||
{ isDivider: true },
|
||||
{
|
||||
label: i18n.translate('xpack.ingestManager.agentDetails.configurationLabel', {
|
||||
defaultMessage: 'Configuration',
|
||||
}),
|
||||
content: isAgentConfigLoading ? (
|
||||
<Loading size="m" />
|
||||
) : agentConfigData?.item ? (
|
||||
<EuiLink href={`${agentConfigUrl}${agentData.item.config_id}`}>
|
||||
{agentConfigData.item.name || agentData.item.config_id}
|
||||
</EuiLink>
|
||||
) : (
|
||||
agentData.item.config_id || '-'
|
||||
),
|
||||
},
|
||||
{ isDivider: true },
|
||||
{
|
||||
content: <AgentDetailsActionMenu agent={agentData.item} />,
|
||||
},
|
||||
].map((item, index) => (
|
||||
<EuiFlexItem grow={false} key={index}>
|
||||
{item.isDivider ?? false ? (
|
||||
<Divider />
|
||||
) : item.label ? (
|
||||
<EuiDescriptionList compressed textStyle="reverse" style={{ textAlign: 'right' }}>
|
||||
<EuiDescriptionListTitle>{item.label}</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>{item.content}</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
) : (
|
||||
item.content
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
undefined
|
||||
),
|
||||
[agentConfigData, agentConfigUrl, agentData, isAgentConfigLoading]
|
||||
);
|
||||
|
||||
const agent = agentRequest.data.item;
|
||||
const headerTabs = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
id: 'activity_log',
|
||||
name: i18n.translate('xpack.ingestManager.agentDetails.subTabs.activityLogTab', {
|
||||
defaultMessage: 'Activity log',
|
||||
}),
|
||||
href: agentActivityTabUrl,
|
||||
isSelected: !tabId || tabId === 'activity',
|
||||
},
|
||||
{
|
||||
id: 'details',
|
||||
name: i18n.translate('xpack.ingestManager.agentDetails.subTabs.detailsTab', {
|
||||
defaultMessage: 'Agent details',
|
||||
}),
|
||||
href: agentDetailsTabUrl,
|
||||
isSelected: tabId === 'details',
|
||||
},
|
||||
];
|
||||
}, [agentActivityTabUrl, agentDetailsTabUrl, tabId]);
|
||||
|
||||
return (
|
||||
<AgentRefreshContext.Provider value={{ refresh: () => agentRequest.sendRequest() }}>
|
||||
<WithHeaderLayout leftColumn={<AgentDetailSection agent={agent} />}>
|
||||
<AgentEventsTable agent={agent} />
|
||||
<AgentRefreshContext.Provider
|
||||
value={{
|
||||
refresh: () => {
|
||||
sendAgentRequest();
|
||||
sendAgentConfigRequest();
|
||||
},
|
||||
}}
|
||||
>
|
||||
<WithHeaderLayout
|
||||
leftColumn={headerLeftContent}
|
||||
rightColumn={headerRightContent}
|
||||
tabs={(headerTabs as unknown) as EuiTabProps[]}
|
||||
>
|
||||
{isLoading && isInitialRequest ? (
|
||||
<Loading />
|
||||
) : error ? (
|
||||
<Error
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.ingestManager.agentDetails.unexceptedErrorTitle"
|
||||
defaultMessage="Error loading agent"
|
||||
/>
|
||||
}
|
||||
error={error}
|
||||
/>
|
||||
) : agentData && agentData.item ? (
|
||||
<Switch>
|
||||
<Route
|
||||
path={`${FLEET_AGENT_DETAIL_PATH}:agentId/details`}
|
||||
render={() => {
|
||||
return (
|
||||
<AgentDetailsContent agent={agentData.item} agentConfig={agentConfigData?.item} />
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
path={`${FLEET_AGENT_DETAIL_PATH}:agentId`}
|
||||
render={() => {
|
||||
return <AgentEventsTable agent={agentData.item} />;
|
||||
}}
|
||||
/>
|
||||
</Switch>
|
||||
) : (
|
||||
<Error
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.ingestManager.agentDetails.agentNotFoundErrorTitle"
|
||||
defaultMessage="Agent not found"
|
||||
/>
|
||||
}
|
||||
error={i18n.translate(
|
||||
'xpack.ingestManager.agentDetails.agentNotFoundErrorDescription',
|
||||
{
|
||||
defaultMessage: 'Cannot found agent ID {agentId}',
|
||||
values: {
|
||||
agentId,
|
||||
},
|
||||
}
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</WithHeaderLayout>
|
||||
</AgentRefreshContext.Provider>
|
||||
);
|
||||
|
|
|
@ -238,13 +238,13 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
|
|||
|
||||
const columns = [
|
||||
{
|
||||
field: 'local_metadata.host.hostname',
|
||||
field: 'local_metadata.host',
|
||||
name: i18n.translate('xpack.ingestManager.agentList.hostColumnTitle', {
|
||||
defaultMessage: 'Host',
|
||||
}),
|
||||
render: (host: string, agent: Agent) => (
|
||||
<ConnectedLink color="primary" path={`${FLEET_AGENT_DETAIL_PATH}${agent.id}`}>
|
||||
{host}
|
||||
{agent.local_metadata['host.hostname'] || host || ''}
|
||||
</ConnectedLink>
|
||||
),
|
||||
},
|
||||
|
@ -308,11 +308,13 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
|
|||
},
|
||||
},
|
||||
{
|
||||
field: 'local_metadata.agent_version',
|
||||
field: 'local_metadata.version',
|
||||
width: '100px',
|
||||
name: i18n.translate('xpack.ingestManager.agentList.versionTitle', {
|
||||
defaultMessage: 'Version',
|
||||
}),
|
||||
render: (version: string, agent: Agent) =>
|
||||
agent.local_metadata['agent.version'] || version || '',
|
||||
},
|
||||
{
|
||||
field: 'last_checkin',
|
||||
|
|
|
@ -8,3 +8,5 @@ export * from './loading';
|
|||
export * from './agent_reassign_config_flyout';
|
||||
export * from './navigation/child_routes';
|
||||
export * from './navigation/connected_link';
|
||||
export * from './agent_health';
|
||||
export * from './agent_unenroll_provider';
|
||||
|
|
|
@ -42,7 +42,7 @@ export const FleetApp: React.FunctionComponent = () => {
|
|||
<Router>
|
||||
<Switch>
|
||||
<Route path="/fleet" exact={true} render={() => <Redirect to="/fleet/agents" />} />
|
||||
<Route path="/fleet/agents/:agentId">
|
||||
<Route path="/fleet/agents/:agentId/:tabId?">
|
||||
<AgentDetailsPage />
|
||||
</Route>
|
||||
<Route path="/fleet/agents">
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import { SavedObjectsClientContract, SavedObjectsBulkCreateObject } from 'src/core/server';
|
||||
import {
|
||||
Agent,
|
||||
NewAgentEvent,
|
||||
AgentEvent,
|
||||
AgentAction,
|
||||
AgentSOAttributes,
|
||||
|
@ -23,7 +24,7 @@ import { appContextService } from '../app_context';
|
|||
export async function agentCheckin(
|
||||
soClient: SavedObjectsClientContract,
|
||||
agent: Agent,
|
||||
events: AgentEvent[],
|
||||
events: NewAgentEvent[],
|
||||
localMetadata?: any
|
||||
) {
|
||||
const updateData: {
|
||||
|
@ -85,10 +86,10 @@ export async function agentCheckin(
|
|||
async function processEventsForCheckin(
|
||||
soClient: SavedObjectsClientContract,
|
||||
agent: Agent,
|
||||
events: AgentEvent[]
|
||||
events: NewAgentEvent[]
|
||||
) {
|
||||
const acknowledgedActionIds: string[] = [];
|
||||
const updatedErrorEvents = [...agent.current_error_events];
|
||||
const updatedErrorEvents: Array<AgentEvent | NewAgentEvent> = [...agent.current_error_events];
|
||||
for (const event of events) {
|
||||
// @ts-ignore
|
||||
event.config_id = agent.config_id;
|
||||
|
@ -122,7 +123,7 @@ async function processEventsForCheckin(
|
|||
async function createEventsForAgent(
|
||||
soClient: SavedObjectsClientContract,
|
||||
agentId: string,
|
||||
events: AgentEvent[]
|
||||
events: NewAgentEvent[]
|
||||
) {
|
||||
const objects: Array<SavedObjectsBulkCreateObject<AgentEventSOAttributes>> = events.map(
|
||||
eventData => {
|
||||
|
@ -139,11 +140,11 @@ async function createEventsForAgent(
|
|||
return soClient.bulkCreate(objects);
|
||||
}
|
||||
|
||||
function isErrorOrState(event: AgentEvent) {
|
||||
function isErrorOrState(event: AgentEvent | NewAgentEvent) {
|
||||
return event.type === 'STATE' || event.type === 'ERROR';
|
||||
}
|
||||
|
||||
function isActionEvent(event: AgentEvent) {
|
||||
function isActionEvent(event: AgentEvent | NewAgentEvent) {
|
||||
return (
|
||||
event.type === 'ACTION' && (event.subtype === 'ACKNOWLEDGED' || event.subtype === 'UNKNOWN')
|
||||
);
|
||||
|
|
|
@ -39,6 +39,7 @@ export async function getAgentEvents(
|
|||
|
||||
const items: AgentEvent[] = saved_objects.map(so => {
|
||||
return {
|
||||
id: so.id,
|
||||
...so.attributes,
|
||||
payload: so.attributes.payload ? JSON.parse(so.attributes.payload) : undefined,
|
||||
};
|
||||
|
|
|
@ -12,6 +12,7 @@ export {
|
|||
AgentSOAttributes,
|
||||
AgentStatus,
|
||||
AgentType,
|
||||
NewAgentEvent,
|
||||
AgentEvent,
|
||||
AgentEventSOAttributes,
|
||||
AgentAction,
|
||||
|
|
|
@ -49,8 +49,13 @@ export const AckEventSchema = schema.object({
|
|||
...{ action_id: schema.string() },
|
||||
});
|
||||
|
||||
export const NewAgentEventSchema = schema.object({
|
||||
...AgentEventBase,
|
||||
});
|
||||
|
||||
export const AgentEventSchema = schema.object({
|
||||
...AgentEventBase,
|
||||
id: schema.string(),
|
||||
});
|
||||
|
||||
export const NewAgentActionSchema = schema.object({
|
||||
|
|
|
@ -5,7 +5,12 @@
|
|||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { AckEventSchema, AgentEventSchema, AgentTypeSchema, NewAgentActionSchema } from '../models';
|
||||
import {
|
||||
AckEventSchema,
|
||||
NewAgentEventSchema,
|
||||
AgentTypeSchema,
|
||||
NewAgentActionSchema,
|
||||
} from '../models';
|
||||
|
||||
export const GetAgentsRequestSchema = {
|
||||
query: schema.object({
|
||||
|
@ -28,7 +33,7 @@ export const PostAgentCheckinRequestSchema = {
|
|||
}),
|
||||
body: schema.object({
|
||||
local_metadata: schema.maybe(schema.recordOf(schema.string(), schema.any())),
|
||||
events: schema.maybe(schema.arrayOf(AgentEventSchema)),
|
||||
events: schema.maybe(schema.arrayOf(NewAgentEventSchema)),
|
||||
}),
|
||||
};
|
||||
|
||||
|
|
|
@ -8213,15 +8213,10 @@
|
|||
"xpack.ingestManager.agentConfigList.revisionNumber": "rev. {revNumber}",
|
||||
"xpack.ingestManager.agentConfigList.updatedOnColumnTitle": "最終更新日:",
|
||||
"xpack.ingestManager.agentConfigList.viewConfigActionText": "構成を表示",
|
||||
"xpack.ingestManager.agentDetails.agentConfigLabel": "AgentConfig",
|
||||
"xpack.ingestManager.agentDetails.agentDetailsTitle": "エージェントの詳細",
|
||||
"xpack.ingestManager.agentDetails.agentNotFoundErrorTitle": "エージェントが見つかりません",
|
||||
"xpack.ingestManager.agentDetails.idLabel": "ID",
|
||||
"xpack.ingestManager.agentDetails.localMetadataSectionSubtitle": "メタデータを読み込み中",
|
||||
"xpack.ingestManager.agentDetails.metadataSectionTitle": "メタデータ",
|
||||
"xpack.ingestManager.agentDetails.statusLabel": "ステータス",
|
||||
"xpack.ingestManager.agentDetails.typeLabel": "タイプ",
|
||||
"xpack.ingestManager.agentDetails.unavailableConfigTooltipText": "この構成は利用できなくなりました",
|
||||
"xpack.ingestManager.agentDetails.unexceptedErrorTitle": "エージェントを読み込む間にエラーが発生しました",
|
||||
"xpack.ingestManager.agentDetails.userProvidedMetadataSectionSubtitle": "ユーザー提供メタデータ",
|
||||
"xpack.ingestManager.agentEnrollment.apiKeySelectionDescription": "ご希望のエージェント構成とプラットフォームをすばやく選択できます。次いで、以下の手順に従ってエージェントをセットアップして登録します。",
|
||||
|
@ -8237,11 +8232,9 @@
|
|||
"xpack.ingestManager.agentEnrollment.stepTestAgents": "エージェントのテスト",
|
||||
"xpack.ingestManager.agentEnrollment.testAgentLoadingMessage": "新しいエージェントの登録を待っています",
|
||||
"xpack.ingestManager.agentEventsList.messageColumnTitle": "メッセージ",
|
||||
"xpack.ingestManager.agentEventsList.paylodColumnTitle": "ペイロード",
|
||||
"xpack.ingestManager.agentEventsList.refreshButton": "更新",
|
||||
"xpack.ingestManager.agentEventsList.subtypeColumnTitle": "サブタイプ",
|
||||
"xpack.ingestManager.agentEventsList.timestampColumnTitle": "タイムスタンプ",
|
||||
"xpack.ingestManager.agentEventsList.title": "アクティビティログ",
|
||||
"xpack.ingestManager.agentEventsList.typeColumnTitle": "タイプ",
|
||||
"xpack.ingestManager.agentHealth.checkInTooltipText": "前回のチェックイン {lastCheckIn}",
|
||||
"xpack.ingestManager.agentHealth.errorStatusText": "エラー",
|
||||
|
|
|
@ -8219,15 +8219,10 @@
|
|||
"xpack.ingestManager.agentConfigList.revisionNumber": "修订 {revNumber}",
|
||||
"xpack.ingestManager.agentConfigList.updatedOnColumnTitle": "最后更新时间",
|
||||
"xpack.ingestManager.agentConfigList.viewConfigActionText": "查看配置",
|
||||
"xpack.ingestManager.agentDetails.agentConfigLabel": "代理配置",
|
||||
"xpack.ingestManager.agentDetails.agentDetailsTitle": "代理详情",
|
||||
"xpack.ingestManager.agentDetails.agentNotFoundErrorTitle": "未找到代理",
|
||||
"xpack.ingestManager.agentDetails.idLabel": "ID",
|
||||
"xpack.ingestManager.agentDetails.localMetadataSectionSubtitle": "本地元数据",
|
||||
"xpack.ingestManager.agentDetails.metadataSectionTitle": "元数据",
|
||||
"xpack.ingestManager.agentDetails.statusLabel": "状态",
|
||||
"xpack.ingestManager.agentDetails.typeLabel": "类型",
|
||||
"xpack.ingestManager.agentDetails.unavailableConfigTooltipText": "此配置不再可用",
|
||||
"xpack.ingestManager.agentDetails.unexceptedErrorTitle": "加载代理时发生错误",
|
||||
"xpack.ingestManager.agentDetails.userProvidedMetadataSectionSubtitle": "用户提供的元数据",
|
||||
"xpack.ingestManager.agentEnrollment.apiKeySelectionDescription": "快速选择所需的代理配置和平台。然后,根据下面的说明设置和注册代理。",
|
||||
|
@ -8243,11 +8238,9 @@
|
|||
"xpack.ingestManager.agentEnrollment.stepTestAgents": "测试代理",
|
||||
"xpack.ingestManager.agentEnrollment.testAgentLoadingMessage": "正在等候新代理注册",
|
||||
"xpack.ingestManager.agentEventsList.messageColumnTitle": "消息",
|
||||
"xpack.ingestManager.agentEventsList.paylodColumnTitle": "负载",
|
||||
"xpack.ingestManager.agentEventsList.refreshButton": "刷新",
|
||||
"xpack.ingestManager.agentEventsList.subtypeColumnTitle": "子类型",
|
||||
"xpack.ingestManager.agentEventsList.timestampColumnTitle": "时间戳",
|
||||
"xpack.ingestManager.agentEventsList.title": "活动日志",
|
||||
"xpack.ingestManager.agentEventsList.typeColumnTitle": "类型",
|
||||
"xpack.ingestManager.agentHealth.checkInTooltipText": "上次签入时间 {lastCheckIn}",
|
||||
"xpack.ingestManager.agentHealth.errorStatusText": "错误",
|
||||
|
|
|
@ -106,7 +106,7 @@ export default function(providerContext: FtrProviderContext) {
|
|||
item.action_id === '48cebde1-c906-4893-b89f-595d943b72a2'
|
||||
);
|
||||
expect(expectedEvents.length).to.eql(2);
|
||||
const expectedEvent = expectedEvents.find(
|
||||
const { id, ...expectedEvent } = expectedEvents.find(
|
||||
(item: Record<string, string>) => item.action_id === '48cebde1-c906-4893-b89f-595d943b72a1'
|
||||
);
|
||||
expect(expectedEvent).to.eql({
|
||||
|
|
Loading…
Reference in a new issue