mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Fleet] Agent activity (#140510)
* WIP: agent activity flyout * added missing info to action_status api and UI * refactored action status api to use aggs for action results * made flyout button visible on selection, refactor * added tour around Agent activity button * formatted texts * query failed actions, review comments * catch errors * updated empty prompt * fix test * added flyout footer * fix test * fix test * updated in progress activity * updated failed activity with started time * added openapi spec, added nbAgentsFailed to action status * small tweak of in progress display and upgrade toast
This commit is contained in:
parent
6aae2e1db8
commit
1709be2d27
26 changed files with 1138 additions and 270 deletions
|
@ -1347,6 +1347,104 @@
|
|||
"operationId": "agents-current-upgrades"
|
||||
}
|
||||
},
|
||||
"/agents/action_status": {
|
||||
"get": {
|
||||
"summary": "Agents - Action status",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/page_size"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/parameters/page_index"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"actionId": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"COMPLETE",
|
||||
"EXPIRED",
|
||||
"CANCELLED",
|
||||
"FAILED",
|
||||
"IN_PROGRESS"
|
||||
]
|
||||
},
|
||||
"nbAgentsActioned": {
|
||||
"type": "number"
|
||||
},
|
||||
"nbAgentsActionCreated": {
|
||||
"type": "number"
|
||||
},
|
||||
"nbAgentsAck": {
|
||||
"type": "number"
|
||||
},
|
||||
"nbAgentsFailed": {
|
||||
"type": "number"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"startTime": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"expiration": {
|
||||
"type": "string"
|
||||
},
|
||||
"completionTime": {
|
||||
"type": "string"
|
||||
},
|
||||
"cancellationTime": {
|
||||
"type": "string"
|
||||
},
|
||||
"newPolicyId": {
|
||||
"type": "string"
|
||||
},
|
||||
"creationTime": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"actionId",
|
||||
"complete",
|
||||
"nbAgentsActioned",
|
||||
"nbAgentsActionCreated",
|
||||
"nbAgentsAck",
|
||||
"nbAgentsFailed",
|
||||
"status",
|
||||
"creationTime"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"items"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"operationId": "agents-action-status"
|
||||
}
|
||||
},
|
||||
"/agents/{agentId}": {
|
||||
"parameters": [
|
||||
{
|
||||
|
|
|
@ -829,6 +829,71 @@ paths:
|
|||
required:
|
||||
- items
|
||||
operationId: agents-current-upgrades
|
||||
/agents/action_status:
|
||||
get:
|
||||
summary: Agents - Action status
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/page_size'
|
||||
- $ref: '#/components/parameters/page_index'
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
actionId:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
enum:
|
||||
- COMPLETE
|
||||
- EXPIRED
|
||||
- CANCELLED
|
||||
- FAILED
|
||||
- IN_PROGRESS
|
||||
nbAgentsActioned:
|
||||
type: number
|
||||
nbAgentsActionCreated:
|
||||
type: number
|
||||
nbAgentsAck:
|
||||
type: number
|
||||
nbAgentsFailed:
|
||||
type: number
|
||||
version:
|
||||
type: string
|
||||
startTime:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
expiration:
|
||||
type: string
|
||||
completionTime:
|
||||
type: string
|
||||
cancellationTime:
|
||||
type: string
|
||||
newPolicyId:
|
||||
type: string
|
||||
creationTime:
|
||||
type: string
|
||||
required:
|
||||
- actionId
|
||||
- complete
|
||||
- nbAgentsActioned
|
||||
- nbAgentsActionCreated
|
||||
- nbAgentsAck
|
||||
- nbAgentsFailed
|
||||
- status
|
||||
- creationTime
|
||||
required:
|
||||
- items
|
||||
operationId: agents-action-status
|
||||
/agents/{agentId}:
|
||||
parameters:
|
||||
- schema:
|
||||
|
|
|
@ -57,6 +57,8 @@ paths:
|
|||
$ref: paths/agents@bulk_upgrade.yaml
|
||||
/agents/current_upgrades:
|
||||
$ref: paths/agents@current_upgrades.yaml
|
||||
/agents/action_status:
|
||||
$ref: paths/agents@action_status.yaml
|
||||
'/agents/{agentId}':
|
||||
$ref: 'paths/agents@{agent_id}.yaml'
|
||||
'/agents/{agentId}/actions':
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
get:
|
||||
summary: Agents - Action status
|
||||
parameters:
|
||||
- $ref: ../components/parameters/page_size.yaml
|
||||
- $ref: ../components/parameters/page_index.yaml
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
actionId:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
enum:
|
||||
- COMPLETE
|
||||
- EXPIRED
|
||||
- CANCELLED
|
||||
- FAILED
|
||||
- IN_PROGRESS
|
||||
nbAgentsActioned:
|
||||
type: number
|
||||
nbAgentsActionCreated:
|
||||
type: number
|
||||
nbAgentsAck:
|
||||
type: number
|
||||
nbAgentsFailed:
|
||||
type: number
|
||||
version:
|
||||
type: string
|
||||
startTime:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
expiration:
|
||||
type: string
|
||||
completionTime:
|
||||
type: string
|
||||
cancellationTime:
|
||||
type: string
|
||||
newPolicyId:
|
||||
type: string
|
||||
creationTime:
|
||||
type: string
|
||||
required:
|
||||
- actionId
|
||||
- complete
|
||||
- nbAgentsActioned
|
||||
- nbAgentsActionCreated
|
||||
- nbAgentsAck
|
||||
- nbAgentsFailed
|
||||
- status
|
||||
- creationTime
|
||||
required:
|
||||
- items
|
||||
operationId: agents-action-status
|
|
@ -112,13 +112,19 @@ export interface ActionStatus {
|
|||
nbAgentsActionCreated: number;
|
||||
// how many agents acknowledged the action sucessfully (completed)
|
||||
nbAgentsAck: number;
|
||||
version: string;
|
||||
// how many agents failed
|
||||
nbAgentsFailed: number;
|
||||
version?: string;
|
||||
startTime?: string;
|
||||
type?: string;
|
||||
// how many agents were actioned by the user
|
||||
nbAgentsActioned: number;
|
||||
status: 'complete' | 'expired' | 'cancelled' | 'failed' | 'in progress';
|
||||
errorMessage?: string;
|
||||
status: 'COMPLETE' | 'EXPIRED' | 'CANCELLED' | 'FAILED' | 'IN_PROGRESS';
|
||||
expiration?: string;
|
||||
completionTime?: string;
|
||||
cancellationTime?: string;
|
||||
newPolicyId?: string;
|
||||
creationTime: string;
|
||||
}
|
||||
|
||||
// Generated from FleetServer schema.json
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* 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 { EuiButtonEmpty, EuiText, EuiTourStep } from '@elastic/eui';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import { useStartServices } from '../../../../hooks';
|
||||
|
||||
export const AgentActivityButton: React.FC<{
|
||||
onClickAgentActivity: () => void;
|
||||
showAgentActivityTour: { isOpen: boolean };
|
||||
}> = ({ onClickAgentActivity, showAgentActivityTour }) => {
|
||||
const { uiSettings } = useStartServices();
|
||||
|
||||
const [agentActivityTourState, setAgentActivityTourState] = useState(showAgentActivityTour);
|
||||
|
||||
const isTourHidden = uiSettings.get('hideAgentActivityTour', false);
|
||||
|
||||
const setTourAsHidden = () => uiSettings.set('hideAgentActivityTour', true);
|
||||
|
||||
useEffect(() => {
|
||||
setAgentActivityTourState(showAgentActivityTour);
|
||||
}, [showAgentActivityTour, setAgentActivityTourState]);
|
||||
|
||||
const button = (
|
||||
<EuiButtonEmpty
|
||||
onClick={() => {
|
||||
onClickAgentActivity();
|
||||
setAgentActivityTourState({ isOpen: false });
|
||||
}}
|
||||
data-test-subj="agentActivityButton"
|
||||
iconType="clock"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentList.agentActivityButton"
|
||||
defaultMessage="Agent activity"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
||||
const onFinish = () => {
|
||||
setAgentActivityTourState({ isOpen: false });
|
||||
setTourAsHidden();
|
||||
};
|
||||
|
||||
return isTourHidden ? (
|
||||
button
|
||||
) : (
|
||||
<EuiTourStep
|
||||
content={
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityButton.tourContent"
|
||||
defaultMessage="Review in progress, completed, and scheduled agent action activity history here anytime."
|
||||
/>
|
||||
</EuiText>
|
||||
}
|
||||
isStepOpen={agentActivityTourState.isOpen}
|
||||
onFinish={() => setAgentActivityTourState({ isOpen: false })}
|
||||
minWidth={360}
|
||||
maxWidth={360}
|
||||
step={1}
|
||||
stepsTotal={1}
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityButton.tourTitle"
|
||||
defaultMessage="Agent activity history"
|
||||
/>
|
||||
}
|
||||
anchorPosition="upCenter"
|
||||
footerAction={<EuiButtonEmpty onClick={onFinish}>OK</EuiButtonEmpty>}
|
||||
>
|
||||
{button}
|
||||
</EuiTourStep>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,528 @@
|
|||
/*
|
||||
* 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 type { ReactNode } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { FormattedDate, FormattedMessage, FormattedTime } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLoadingSpinner,
|
||||
EuiIcon,
|
||||
EuiFlyout,
|
||||
EuiFlyoutHeader,
|
||||
EuiFlyoutBody,
|
||||
EuiTitle,
|
||||
EuiText,
|
||||
EuiPanel,
|
||||
EuiButton,
|
||||
EuiLink,
|
||||
EuiEmptyPrompt,
|
||||
EuiButtonEmpty,
|
||||
EuiFlyoutFooter,
|
||||
} from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import type { ActionStatus } from '../../../../types';
|
||||
import { useActionStatus } from '../hooks';
|
||||
import { useGetAgentPolicies, useStartServices } from '../../../../hooks';
|
||||
import { SO_SEARCH_LIMIT } from '../../../../constants';
|
||||
|
||||
import { getTodayActions, getOtherDaysActions } from './agent_activity_helper';
|
||||
|
||||
const FullHeightFlyoutBody = styled(EuiFlyoutBody)`
|
||||
.euiFlyoutBody__overflowContent {
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const FlyoutFooterWPadding = styled(EuiFlyoutFooter)`
|
||||
padding: 16px 24px !important;
|
||||
`;
|
||||
|
||||
export const AgentActivityFlyout: React.FunctionComponent<{
|
||||
onClose: () => void;
|
||||
onAbortSuccess: () => void;
|
||||
}> = ({ onClose, onAbortSuccess }) => {
|
||||
const { data: agentPoliciesData } = useGetAgentPolicies({
|
||||
perPage: SO_SEARCH_LIMIT,
|
||||
});
|
||||
|
||||
const { currentActions, abortUpgrade } = useActionStatus(onAbortSuccess);
|
||||
|
||||
const getAgentPolicyName = (policyId: string) => {
|
||||
const policy = agentPoliciesData?.items.find((item) => item.id === policyId);
|
||||
return policy?.name ?? policyId;
|
||||
};
|
||||
|
||||
const currentActionsEnriched = currentActions.map((a) => ({
|
||||
...a,
|
||||
newPolicyId: getAgentPolicyName(a.newPolicyId ?? ''),
|
||||
}));
|
||||
|
||||
const inProgressActions = currentActionsEnriched.filter((a) => a.status === 'IN_PROGRESS');
|
||||
|
||||
const completedActions = currentActionsEnriched.filter((a) => a.status !== 'IN_PROGRESS');
|
||||
|
||||
const todayActions = getTodayActions(completedActions);
|
||||
const otherDays = getOtherDaysActions(completedActions);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlyout data-test-subj="agentActivityFlyout" onClose={onClose} size="m" paddingSize="none">
|
||||
<EuiFlyoutHeader aria-labelledby="FleetAgentActivityFlyoutTitle">
|
||||
<EuiPanel borderRadius="none" hasShadow={false} hasBorder={true}>
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="l">
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityFlyout.title"
|
||||
defaultMessage="Agent activity"
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText color="subdued">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityFlyout.activityLogText"
|
||||
defaultMessage="Activity log of Elastic Agent operations will appear here."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</EuiFlyoutHeader>
|
||||
|
||||
<FullHeightFlyoutBody>
|
||||
{currentActionsEnriched.length === 0 ? (
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
justifyContent={'center'}
|
||||
alignItems={'center'}
|
||||
className="eui-fullHeight"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiEmptyPrompt
|
||||
iconType="clock"
|
||||
iconColor="default"
|
||||
title={
|
||||
<h2>
|
||||
{' '}
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityFlyout.noActivityText"
|
||||
defaultMessage="No activity to display"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
titleSize="m"
|
||||
body={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityFlyout.noActivityDescription"
|
||||
defaultMessage="Activity feed will appear here as agents get enrolled, upgraded, or configured."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : null}
|
||||
{inProgressActions.length > 0 ? (
|
||||
<ActivitySection
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityFlyout.inProgressTitle"
|
||||
defaultMessage="In progress"
|
||||
/>
|
||||
}
|
||||
actions={inProgressActions}
|
||||
abortUpgrade={abortUpgrade}
|
||||
/>
|
||||
) : null}
|
||||
{todayActions.length > 0 ? (
|
||||
<ActivitySection
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityFlyout.todayTitle"
|
||||
defaultMessage="Today"
|
||||
/>
|
||||
}
|
||||
actions={todayActions}
|
||||
abortUpgrade={abortUpgrade}
|
||||
/>
|
||||
) : null}
|
||||
{Object.keys(otherDays).map((day) => (
|
||||
<ActivitySection
|
||||
title={<FormattedDate value={day} year="numeric" month="short" day="2-digit" />}
|
||||
actions={otherDays[day]}
|
||||
abortUpgrade={abortUpgrade}
|
||||
/>
|
||||
))}
|
||||
</FullHeightFlyoutBody>
|
||||
<FlyoutFooterWPadding>
|
||||
<EuiFlexGroup justifyContent="flexStart">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty onClick={onClose}>
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityFlyout.closeBtn"
|
||||
defaultMessage="Close"
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</FlyoutFooterWPadding>
|
||||
</EuiFlyout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ActivitySection: React.FunctionComponent<{
|
||||
title: ReactNode;
|
||||
actions: ActionStatus[];
|
||||
abortUpgrade: (action: ActionStatus) => Promise<void>;
|
||||
}> = ({ title, actions, abortUpgrade }) => {
|
||||
return (
|
||||
<>
|
||||
<EuiPanel color="subdued" hasBorder={true} borderRadius="none">
|
||||
<EuiText>
|
||||
<b>{title}</b>
|
||||
</EuiText>
|
||||
</EuiPanel>
|
||||
{actions.map((currentAction) =>
|
||||
currentAction.type === 'UPGRADE' && currentAction.status === 'IN_PROGRESS' ? (
|
||||
<UpgradeInProgressActivityItem action={currentAction} abortUpgrade={abortUpgrade} />
|
||||
) : (
|
||||
<ActivityItem action={currentAction} />
|
||||
)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const actionNames: {
|
||||
[key: string]: { inProgressText: string; completedText: string; cancelledText: string };
|
||||
} = {
|
||||
POLICY_REASSIGN: {
|
||||
inProgressText: 'Reassigning',
|
||||
completedText: 'assigned to a new policy',
|
||||
cancelledText: 'assignment',
|
||||
},
|
||||
UPGRADE: { inProgressText: 'Upgrading', completedText: 'upgraded', cancelledText: 'upgrade' },
|
||||
UNENROLL: {
|
||||
inProgressText: 'Unenrolling',
|
||||
completedText: 'unenrolled',
|
||||
cancelledText: 'unenrollment',
|
||||
},
|
||||
CANCEL: { inProgressText: 'Cancelling', completedText: 'cancelled', cancelledText: '' },
|
||||
ACTION: { inProgressText: 'Actioning', completedText: 'actioned', cancelledText: 'action' },
|
||||
};
|
||||
|
||||
const inProgressTitleColor = '#0077CC';
|
||||
|
||||
const formattedTime = (time?: string) => {
|
||||
return time ? (
|
||||
<>
|
||||
<FormattedDate value={time} year="numeric" month="short" day="2-digit" />
|
||||
|
||||
<FormattedTime value={time} />
|
||||
</>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const inProgressTitle = (action: ActionStatus) => (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivity.inProgressTitle"
|
||||
defaultMessage="{inProgressText} {nbAgents} {agents} {reassignText}{upgradeText}"
|
||||
values={{
|
||||
nbAgents:
|
||||
action.nbAgentsAck === action.nbAgentsActioned
|
||||
? action.nbAgentsAck
|
||||
: action.nbAgentsAck === 0
|
||||
? action.nbAgentsActioned
|
||||
: action.nbAgentsActioned - action.nbAgentsAck + ' of ' + action.nbAgentsActioned,
|
||||
agents: action.nbAgentsActioned === 1 ? 'agent' : 'agents',
|
||||
inProgressText: actionNames[action.type ?? 'ACTION'].inProgressText,
|
||||
reassignText:
|
||||
action.type === 'POLICY_REASSIGN' && action.newPolicyId ? `to ${action.newPolicyId}` : '',
|
||||
upgradeText: action.type === 'UPGRADE' ? `to version ${action.version}` : '',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const inProgressDescription = (time?: string) => (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityFlyout.startedDescription"
|
||||
defaultMessage="Started on {date}."
|
||||
values={{
|
||||
date: formattedTime(time),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const ActivityItem: React.FunctionComponent<{ action: ActionStatus }> = ({ action }) => {
|
||||
const completeTitle = (
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivity.completedTitle"
|
||||
defaultMessage="{nbAgents} {agents} {completedText}"
|
||||
values={{
|
||||
nbAgents:
|
||||
action.nbAgentsAck === action.nbAgentsActioned
|
||||
? action.nbAgentsAck
|
||||
: action.nbAgentsAck + ' of ' + action.nbAgentsActioned,
|
||||
agents: action.nbAgentsActioned === 1 ? 'agent' : 'agents',
|
||||
completedText: actionNames[action.type ?? 'ACTION'].completedText,
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
);
|
||||
|
||||
const completedDescription = (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityFlyout.completedDescription"
|
||||
defaultMessage="Completed {date}"
|
||||
values={{
|
||||
date: formattedTime(action.completionTime),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const displayByStatus: {
|
||||
[key: string]: {
|
||||
icon: ReactNode;
|
||||
title: ReactNode;
|
||||
titleColor: string;
|
||||
description: ReactNode | null;
|
||||
};
|
||||
} = {
|
||||
IN_PROGRESS: {
|
||||
icon: <EuiLoadingSpinner size="m" />,
|
||||
title: <EuiText>{inProgressTitle(action)}</EuiText>,
|
||||
titleColor: inProgressTitleColor,
|
||||
description: <EuiText color="subdued">{inProgressDescription(action.creationTime)}</EuiText>,
|
||||
},
|
||||
COMPLETE: {
|
||||
icon: <EuiIcon size="m" type="checkInCircleFilled" color="green" />,
|
||||
title: completeTitle,
|
||||
titleColor: 'green',
|
||||
description:
|
||||
action.type === 'POLICY_REASSIGN' && action.newPolicyId ? (
|
||||
<EuiText color="subdued">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityFlyout.reassignCompletedDescription"
|
||||
defaultMessage="Assigned to {policy}."
|
||||
values={{
|
||||
policy: action.newPolicyId,
|
||||
}}
|
||||
/>{' '}
|
||||
{completedDescription}
|
||||
</p>
|
||||
</EuiText>
|
||||
) : (
|
||||
<EuiText color="subdued">{completedDescription}</EuiText>
|
||||
),
|
||||
},
|
||||
FAILED: {
|
||||
icon: <EuiIcon size="m" type="alert" color="red" />,
|
||||
title: completeTitle,
|
||||
titleColor: 'red',
|
||||
description: (
|
||||
<EuiText color="subdued">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityFlyout.failureDescription"
|
||||
defaultMessage=" A problem occured during this operation."
|
||||
/>
|
||||
|
||||
{inProgressDescription(action.creationTime)}
|
||||
</p>
|
||||
</EuiText>
|
||||
),
|
||||
},
|
||||
CANCELLED: {
|
||||
icon: <EuiIcon size="m" type="alert" color="grey" />,
|
||||
titleColor: 'grey',
|
||||
title: (
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityFlyout.cancelledTitle"
|
||||
defaultMessage="Agent {cancelledText} cancelled"
|
||||
values={{
|
||||
cancelledText: actionNames[action.type ?? 'ACTION'].cancelledText,
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
),
|
||||
description: (
|
||||
<EuiText color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityFlyout.cancelledDescription"
|
||||
defaultMessage="Cancelled on {date}"
|
||||
values={{
|
||||
date: formattedTime(action.cancellationTime),
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
),
|
||||
},
|
||||
EXPIRED: {
|
||||
icon: <EuiIcon size="m" type="alert" color="grey" />,
|
||||
titleColor: 'grey',
|
||||
title: (
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityFlyout.expiredTitle"
|
||||
defaultMessage="Agent {expiredText} expired"
|
||||
values={{
|
||||
expiredText: actionNames[action.type ?? 'ACTION'].cancelledText,
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
),
|
||||
description: (
|
||||
<EuiText color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityFlyout.expiredDescription"
|
||||
defaultMessage="Expired on {date}"
|
||||
values={{
|
||||
date: formattedTime(action.expiration),
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiPanel hasBorder={true} borderRadius="none">
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="row" gutterSize="m" alignItems="center">
|
||||
<EuiFlexItem grow={false}>{displayByStatus[action.status].icon}</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText color={displayByStatus[action.status].titleColor}>
|
||||
{displayByStatus[action.status].title}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText color="subdued">{displayByStatus[action.status].description}</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
||||
export const UpgradeInProgressActivityItem: React.FunctionComponent<{
|
||||
action: ActionStatus;
|
||||
abortUpgrade: (action: ActionStatus) => Promise<void>;
|
||||
}> = ({ action, abortUpgrade }) => {
|
||||
const { docLinks } = useStartServices();
|
||||
const [isAborting, setIsAborting] = useState(false);
|
||||
const onClickAbortUpgrade = useCallback(async () => {
|
||||
try {
|
||||
setIsAborting(true);
|
||||
await abortUpgrade(action);
|
||||
} finally {
|
||||
setIsAborting(false);
|
||||
}
|
||||
}, [action, abortUpgrade]);
|
||||
|
||||
const isScheduled = useMemo(() => {
|
||||
if (!action.startTime) {
|
||||
return false;
|
||||
}
|
||||
const now = Date.now();
|
||||
const startDate = new Date(action.startTime).getTime();
|
||||
|
||||
return startDate > now;
|
||||
}, [action]);
|
||||
|
||||
return (
|
||||
<EuiPanel hasBorder={true} borderRadius="none">
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="row" gutterSize="m" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
{isScheduled ? <EuiIcon type="clock" /> : <EuiLoadingSpinner size="m" />}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText color={inProgressTitleColor}>
|
||||
{isScheduled && action.startTime ? (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityFlyout.scheduleTitle"
|
||||
defaultMessage="{nbAgents} agents scheduled to upgrade to version {version}"
|
||||
values={{
|
||||
nbAgents: action.nbAgentsActioned - action.nbAgentsAck,
|
||||
version: action.version,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
inProgressTitle(action)
|
||||
)}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="column" alignItems="flexStart">
|
||||
<EuiFlexItem>
|
||||
<EuiText color="subdued">
|
||||
<p>
|
||||
{isScheduled && action.startTime ? (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityFlyout.scheduledDescription"
|
||||
defaultMessage="Scheduled for "
|
||||
/>
|
||||
<strong>{formattedTime(action.startTime)}</strong>.
|
||||
</>
|
||||
) : (
|
||||
<>{inProgressDescription(action.creationTime)} </>
|
||||
)}
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityFlyout.upgradeDescription"
|
||||
defaultMessage="{guideLink} about agent upgrades."
|
||||
values={{
|
||||
guideLink: (
|
||||
<EuiLink href={docLinks.links.fleet.upgradeElasticAgent} target="_blank">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityFlyout.guideLink"
|
||||
defaultMessage="Learn more"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
size="s"
|
||||
onClick={onClickAbortUpgrade}
|
||||
isLoading={isAborting}
|
||||
data-test-subj="currentBulkUpgrade.abortBtn"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityFlyout.abortUpgradeButtom"
|
||||
defaultMessage="Abort upgrade"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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 type { ActionStatus } from '../../../../types';
|
||||
|
||||
import { getOtherDaysActions, getTodayActions } from './agent_activity_helper';
|
||||
|
||||
describe('agent activity helper', () => {
|
||||
const actions = [
|
||||
{ creationTime: '2022-09-14T14:44:23.501Z' },
|
||||
{ creationTime: '2022-09-14T11:44:23.501Z' },
|
||||
{ creationTime: '2022-09-12T14:44:23.501Z' },
|
||||
{ creationTime: '2022-09-11T14:44:23.501Z' },
|
||||
{ creationTime: '2022-09-11T10:44:23.501Z' },
|
||||
] as ActionStatus[];
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers('modern').setSystemTime(new Date('2022-09-14'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should filter today actions', () => {
|
||||
const result = getTodayActions(actions);
|
||||
expect(result.length).toEqual(2);
|
||||
});
|
||||
|
||||
it('should filter other day actions', () => {
|
||||
const result = getOtherDaysActions(actions);
|
||||
expect(Object.keys(result)).toEqual(['2022-09-12', '2022-09-11']);
|
||||
expect(result['2022-09-12'].length).toEqual(1);
|
||||
expect(result['2022-09-11'].length).toEqual(2);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 type { ActionStatus } from '../../../../types';
|
||||
|
||||
const today = () => new Date().toISOString().substring(0, 10);
|
||||
|
||||
export function getOtherDaysActions(actions: ActionStatus[]) {
|
||||
const otherDays: { [day: string]: ActionStatus[] } = {};
|
||||
actions
|
||||
.filter((a) => !a.creationTime.startsWith(today()))
|
||||
.forEach((action) => {
|
||||
const day = action.creationTime.substring(0, 10);
|
||||
if (!otherDays[day]) {
|
||||
otherDays[day] = [];
|
||||
}
|
||||
otherDays[day].push(action);
|
||||
});
|
||||
return otherDays;
|
||||
}
|
||||
|
||||
export function getTodayActions(actions: ActionStatus[]) {
|
||||
const todayActions = actions.filter((a) => a.creationTime.startsWith(today()));
|
||||
return todayActions;
|
||||
}
|
|
@ -1,130 +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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useState, useMemo } from 'react';
|
||||
import { FormattedMessage, FormattedDate, FormattedTime } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiCallOut,
|
||||
EuiLink,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButton,
|
||||
EuiLoadingSpinner,
|
||||
EuiIcon,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { useStartServices } from '../../../../hooks';
|
||||
import type { CurrentUpgrade } from '../../../../types';
|
||||
|
||||
export interface CurrentBulkUpgradeCalloutProps {
|
||||
currentUpgrade: CurrentUpgrade;
|
||||
abortUpgrade: (currentUpgrade: CurrentUpgrade) => Promise<void>;
|
||||
}
|
||||
|
||||
export const CurrentBulkUpgradeCallout: React.FunctionComponent<CurrentBulkUpgradeCalloutProps> = ({
|
||||
currentUpgrade,
|
||||
abortUpgrade,
|
||||
}) => {
|
||||
const { docLinks } = useStartServices();
|
||||
const [isAborting, setIsAborting] = useState(false);
|
||||
const onClickAbortUpgrade = useCallback(async () => {
|
||||
try {
|
||||
setIsAborting(true);
|
||||
await abortUpgrade(currentUpgrade);
|
||||
} finally {
|
||||
setIsAborting(false);
|
||||
}
|
||||
}, [currentUpgrade, abortUpgrade]);
|
||||
|
||||
const isScheduled = useMemo(() => {
|
||||
if (!currentUpgrade.startTime) {
|
||||
return false;
|
||||
}
|
||||
const now = Date.now();
|
||||
const startDate = new Date(currentUpgrade.startTime).getTime();
|
||||
|
||||
return startDate > now;
|
||||
}, [currentUpgrade]);
|
||||
|
||||
const calloutTitle =
|
||||
isScheduled && currentUpgrade.startTime ? (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.currentUpgrade.scheduleCalloutTitle"
|
||||
defaultMessage="{nbAgents} agents scheduled to upgrade to version {version} on {date}"
|
||||
values={{
|
||||
nbAgents: currentUpgrade.nbAgents - currentUpgrade.nbAgentsAck,
|
||||
version: currentUpgrade.version,
|
||||
date: (
|
||||
<>
|
||||
<FormattedDate
|
||||
value={currentUpgrade.startTime}
|
||||
year="numeric"
|
||||
month="short"
|
||||
day="2-digit"
|
||||
/>
|
||||
|
||||
<FormattedTime value={currentUpgrade.startTime} />
|
||||
</>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.currentUpgrade.calloutTitle"
|
||||
defaultMessage="Upgrading {nbAgents, plural, one {# agent} other {# agents}} to version {version}"
|
||||
values={{
|
||||
nbAgents: currentUpgrade.nbAgents - currentUpgrade.nbAgentsAck,
|
||||
version: currentUpgrade.version,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<EuiCallOut color="primary">
|
||||
<EuiFlexGroup
|
||||
className="euiCallOutHeader__title"
|
||||
justifyContent="spaceBetween"
|
||||
alignItems="center"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<div>
|
||||
{isScheduled ? <EuiIcon type="iInCircle" /> : <EuiLoadingSpinner />}
|
||||
|
||||
{calloutTitle}
|
||||
</div>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
size="s"
|
||||
onClick={onClickAbortUpgrade}
|
||||
isLoading={isAborting}
|
||||
data-test-subj="currentBulkUpgrade.sbortBtn"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.currentUpgrade.abortUpgradeButtom"
|
||||
defaultMessage="Abort upgrade"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.currentUpgrade.calloutDescription"
|
||||
defaultMessage="For more information see the {guideLink}."
|
||||
values={{
|
||||
guideLink: (
|
||||
<EuiLink href={docLinks.links.fleet.fleetServerAddFleetServer} target="_blank" external>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.currentUpgrade.guideLink"
|
||||
defaultMessage="Fleet and Elastic Agent Guide"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiCallOut>
|
||||
);
|
||||
};
|
|
@ -5,5 +5,5 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { CurrentBulkUpgradeCallout } from './current_bulk_upgrade_callout';
|
||||
export type { CurrentBulkUpgradeCalloutProps } from './current_bulk_upgrade_callout';
|
||||
export { AgentActivityFlyout } from './agent_activity_flyout';
|
||||
export { AgentActivityButton } from './agent_activity_button';
|
||||
|
|
|
@ -30,6 +30,7 @@ import { MAX_TAG_DISPLAY_LENGTH, truncateTag } from '../utils';
|
|||
|
||||
import { AgentBulkActions } from './bulk_actions';
|
||||
import type { SelectionMode } from './types';
|
||||
import { AgentActivityButton } from './agent_activity_button';
|
||||
|
||||
const statusFilters = [
|
||||
{
|
||||
|
@ -91,6 +92,8 @@ export const SearchAndFilterBar: React.FunctionComponent<{
|
|||
onClickAddAgent: () => void;
|
||||
onClickAddFleetServer: () => void;
|
||||
visibleAgents: Agent[];
|
||||
onClickAgentActivity: () => void;
|
||||
showAgentActivityTour: { isOpen: boolean };
|
||||
}> = ({
|
||||
agentPolicies,
|
||||
draftKuery,
|
||||
|
@ -114,6 +117,8 @@ export const SearchAndFilterBar: React.FunctionComponent<{
|
|||
onClickAddAgent,
|
||||
onClickAddFleetServer,
|
||||
visibleAgents,
|
||||
onClickAgentActivity,
|
||||
showAgentActivityTour,
|
||||
}) => {
|
||||
// Policies state for filtering
|
||||
const [isAgentPoliciesFilterOpen, setIsAgentPoliciesFilterOpen] = useState<boolean>(false);
|
||||
|
@ -323,6 +328,12 @@ export const SearchAndFilterBar: React.FunctionComponent<{
|
|||
</EuiFilterButton>
|
||||
</EuiFilterGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<AgentActivityButton
|
||||
onClickAgentActivity={onClickAgentActivity}
|
||||
showAgentActivityTour={showAgentActivityTour}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{selectedAgents.length === 0 && (
|
||||
<>
|
||||
<EuiFlexItem>
|
||||
|
|
|
@ -5,5 +5,5 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { useCurrentUpgrades } from './use_current_upgrades';
|
||||
export { useUpdateTags } from './use_update_tags';
|
||||
export { useActionStatus } from './use_action_status';
|
||||
|
|
|
@ -8,21 +8,21 @@
|
|||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { sendGetCurrentUpgrades, sendPostCancelAction, useStartServices } from '../../../../hooks';
|
||||
import { sendGetActionStatus, sendPostCancelAction, useStartServices } from '../../../../hooks';
|
||||
|
||||
import type { CurrentUpgrade } from '../../../../types';
|
||||
import type { ActionStatus } from '../../../../types';
|
||||
|
||||
const POLL_INTERVAL = 2 * 60 * 1000; // 2 minutes
|
||||
const POLL_INTERVAL = 30 * 1000;
|
||||
|
||||
export function useCurrentUpgrades(onAbortSuccess: () => void) {
|
||||
const [currentUpgrades, setCurrentUpgrades] = useState<CurrentUpgrade[]>([]);
|
||||
export function useActionStatus(onAbortSuccess: () => void) {
|
||||
const [currentActions, setCurrentActions] = useState<ActionStatus[]>([]);
|
||||
const currentTimeoutRef = useRef<NodeJS.Timeout>();
|
||||
const isCancelledRef = useRef<boolean>(false);
|
||||
const { notifications, overlays } = useStartServices();
|
||||
|
||||
const refreshUpgrades = useCallback(async () => {
|
||||
const refreshActions = useCallback(async () => {
|
||||
try {
|
||||
const res = await sendGetCurrentUpgrades();
|
||||
const res = await sendGetActionStatus();
|
||||
if (isCancelledRef.current) {
|
||||
return;
|
||||
}
|
||||
|
@ -34,55 +34,22 @@ export function useCurrentUpgrades(onAbortSuccess: () => void) {
|
|||
throw new Error('No data');
|
||||
}
|
||||
|
||||
setCurrentUpgrades(res.data.items);
|
||||
setCurrentActions(res.data.items);
|
||||
} catch (err) {
|
||||
notifications.toasts.addError(err, {
|
||||
title: i18n.translate('xpack.fleet.currentUpgrade.fetchRequestError', {
|
||||
defaultMessage: 'An error happened while fetching current upgrades',
|
||||
title: i18n.translate('xpack.fleet.actionStatus.fetchRequestError', {
|
||||
defaultMessage: 'An error happened while fetching action status',
|
||||
}),
|
||||
});
|
||||
}
|
||||
}, [notifications.toasts]);
|
||||
|
||||
const abortUpgrade = useCallback(
|
||||
async (currentUpgrade: CurrentUpgrade) => {
|
||||
try {
|
||||
const confirmRes = await overlays.openConfirm(
|
||||
i18n.translate('xpack.fleet.currentUpgrade.confirmDescription', {
|
||||
defaultMessage: 'This action will abort upgrade of {nbAgents} agents',
|
||||
values: {
|
||||
nbAgents: currentUpgrade.nbAgents - currentUpgrade.nbAgentsAck,
|
||||
},
|
||||
}),
|
||||
{
|
||||
title: i18n.translate('xpack.fleet.currentUpgrade.confirmTitle', {
|
||||
defaultMessage: 'Abort upgrade?',
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
if (!confirmRes) {
|
||||
return;
|
||||
}
|
||||
await sendPostCancelAction(currentUpgrade.actionId);
|
||||
await Promise.all([refreshUpgrades(), onAbortSuccess()]);
|
||||
} catch (err) {
|
||||
notifications.toasts.addError(err, {
|
||||
title: i18n.translate('xpack.fleet.currentUpgrade.abortRequestError', {
|
||||
defaultMessage: 'An error happened while aborting upgrade',
|
||||
}),
|
||||
});
|
||||
}
|
||||
},
|
||||
[refreshUpgrades, notifications.toasts, overlays, onAbortSuccess]
|
||||
);
|
||||
|
||||
// Poll for upgrades
|
||||
useEffect(() => {
|
||||
isCancelledRef.current = false;
|
||||
|
||||
async function pollData() {
|
||||
await refreshUpgrades();
|
||||
await refreshActions();
|
||||
if (isCancelledRef.current) {
|
||||
return;
|
||||
}
|
||||
|
@ -98,11 +65,44 @@ export function useCurrentUpgrades(onAbortSuccess: () => void) {
|
|||
clearTimeout(currentTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, [refreshUpgrades]);
|
||||
}, [refreshActions]);
|
||||
|
||||
const abortUpgrade = useCallback(
|
||||
async (action: ActionStatus) => {
|
||||
try {
|
||||
const confirmRes = await overlays.openConfirm(
|
||||
i18n.translate('xpack.fleet.currentUpgrade.confirmDescription', {
|
||||
defaultMessage: 'This action will abort upgrade of {nbAgents} agents',
|
||||
values: {
|
||||
nbAgents: action.nbAgentsActioned - action.nbAgentsAck,
|
||||
},
|
||||
}),
|
||||
{
|
||||
title: i18n.translate('xpack.fleet.currentUpgrade.confirmTitle', {
|
||||
defaultMessage: 'Abort upgrade?',
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
if (!confirmRes) {
|
||||
return;
|
||||
}
|
||||
await sendPostCancelAction(action.actionId);
|
||||
await Promise.all([refreshActions(), onAbortSuccess()]);
|
||||
} catch (err) {
|
||||
notifications.toasts.addError(err, {
|
||||
title: i18n.translate('xpack.fleet.currentUpgrade.abortRequestError', {
|
||||
defaultMessage: 'An error happened while aborting upgrade',
|
||||
}),
|
||||
});
|
||||
}
|
||||
},
|
||||
[refreshActions, notifications.toasts, overlays, onAbortSuccess]
|
||||
);
|
||||
|
||||
return {
|
||||
currentUpgrades,
|
||||
refreshUpgrades,
|
||||
currentActions,
|
||||
refreshActions,
|
||||
abortUpgrade,
|
||||
};
|
||||
}
|
|
@ -51,7 +51,6 @@ import {
|
|||
} from '../components';
|
||||
import { useFleetServerUnhealthy } from '../hooks/use_fleet_server_unhealthy';
|
||||
|
||||
import { CurrentBulkUpgradeCallout } from './components';
|
||||
import { AgentTableHeader } from './components/table_header';
|
||||
import type { SelectionMode } from './components/types';
|
||||
import { SearchAndFilterBar } from './components/search_and_filter_bar';
|
||||
|
@ -59,7 +58,7 @@ import { Tags } from './components/tags';
|
|||
import { TagsAddRemove } from './components/tags_add_remove';
|
||||
import { TableRowActions } from './components/table_row_actions';
|
||||
import { EmptyPrompt } from './components/empty_prompt';
|
||||
import { useCurrentUpgrades } from './hooks';
|
||||
import { AgentActivityFlyout } from './components';
|
||||
|
||||
const REFRESH_INTERVAL_MS = 30000;
|
||||
|
||||
|
@ -136,6 +135,8 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
|
|||
isOpen: false,
|
||||
});
|
||||
|
||||
const [isAgentActivityFlyoutOpen, setAgentActivityFlyoutOpen] = useState(false);
|
||||
|
||||
const flyoutContext = useFlyoutContext();
|
||||
|
||||
// Agent actions states
|
||||
|
@ -210,6 +211,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
|
|||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [totalAgents, setTotalAgents] = useState(0);
|
||||
const [totalInactiveAgents, setTotalInactiveAgents] = useState(0);
|
||||
const [showAgentActivityTour, setShowAgentActivityTour] = useState({ isOpen: false });
|
||||
|
||||
const getSortFieldForAPI = (field: keyof Agent): string => {
|
||||
if ([VERSION_FIELD, HOSTNAME_FIELD].includes(field as string)) {
|
||||
|
@ -402,8 +404,9 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
|
|||
flyoutContext.openFleetServerFlyout();
|
||||
}, [flyoutContext]);
|
||||
|
||||
// Current upgrades
|
||||
const { abortUpgrade, currentUpgrades, refreshUpgrades } = useCurrentUpgrades(fetchData);
|
||||
const onClickAgentActivity = useCallback(() => {
|
||||
setAgentActivityFlyoutOpen(true);
|
||||
}, [setAgentActivityFlyoutOpen]);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
|
@ -546,6 +549,14 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
{isAgentActivityFlyoutOpen ? (
|
||||
<EuiPortal>
|
||||
<AgentActivityFlyout
|
||||
onAbortSuccess={fetchData}
|
||||
onClose={() => setAgentActivityFlyoutOpen(false)}
|
||||
/>
|
||||
</EuiPortal>
|
||||
) : null}
|
||||
{enrollmentFlyout.isOpen ? (
|
||||
<EuiPortal>
|
||||
<AgentEnrollmentFlyout
|
||||
|
@ -591,7 +602,6 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
|
|||
onClose={() => {
|
||||
setAgentToUpgrade(undefined);
|
||||
fetchData();
|
||||
refreshUpgrades();
|
||||
}}
|
||||
/>
|
||||
</EuiPortal>
|
||||
|
@ -620,13 +630,6 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
|
|||
<EuiSpacer size="l" />
|
||||
</>
|
||||
)}
|
||||
{/* Current upgrades callout */}
|
||||
{currentUpgrades.map((currentUpgrade) => (
|
||||
<React.Fragment key={currentUpgrade.actionId}>
|
||||
<CurrentBulkUpgradeCallout currentUpgrade={currentUpgrade} abortUpgrade={abortUpgrade} />
|
||||
<EuiSpacer size="l" />
|
||||
</React.Fragment>
|
||||
))}
|
||||
{/* Search and filter bar */}
|
||||
<SearchAndFilterBar
|
||||
agentPolicies={agentPolicies}
|
||||
|
@ -647,12 +650,15 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
|
|||
selectionMode={selectionMode}
|
||||
currentQuery={kuery}
|
||||
selectedAgents={selectedAgents}
|
||||
refreshAgents={({ refreshTags = false }: { refreshTags?: boolean } = {}) =>
|
||||
Promise.all([fetchData({ refreshTags }), refreshUpgrades()])
|
||||
}
|
||||
refreshAgents={({ refreshTags = false }: { refreshTags?: boolean } = {}) => {
|
||||
Promise.all([fetchData({ refreshTags })]);
|
||||
setShowAgentActivityTour({ isOpen: true });
|
||||
}}
|
||||
onClickAddAgent={() => setEnrollmentFlyoutState({ isOpen: true })}
|
||||
onClickAddFleetServer={onClickAddFleetServer}
|
||||
visibleAgents={agents}
|
||||
onClickAgentActivity={onClickAgentActivity}
|
||||
showAgentActivityTour={showAgentActivityTour}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
{/* Agent total, bulk actions and status bar */}
|
||||
|
|
|
@ -202,11 +202,11 @@ export const AgentUpgradeAgentModal: React.FunctionComponent<AgentUpgradeAgentMo
|
|||
|
||||
if (!hasCompleted) {
|
||||
notifications.toasts.addSuccess(submittedMessage);
|
||||
} else if (isSingleAgent && counts.success === counts.total) {
|
||||
} else if (counts.success === counts.total) {
|
||||
notifications.toasts.addSuccess(
|
||||
i18n.translate('xpack.fleet.upgradeAgents.successSingleNotificationTitle', {
|
||||
defaultMessage: 'Upgrading {count} agent',
|
||||
values: { count: 1 },
|
||||
defaultMessage: 'Upgrading {count, plural, one {# agent} other {# agents}}',
|
||||
values: { count: isSingleAgent ? 1 : counts.total },
|
||||
})
|
||||
);
|
||||
} else if (counts.error === counts.total) {
|
||||
|
|
|
@ -42,6 +42,7 @@ import type {
|
|||
PutAgentReassignRequestSchema,
|
||||
PostBulkAgentReassignRequestSchema,
|
||||
PostBulkUpdateAgentTagsRequestSchema,
|
||||
GetActionStatusRequestSchema,
|
||||
} from '../../types';
|
||||
import { defaultIngestErrorHandler } from '../../errors';
|
||||
import * as AgentService from '../../services/agents';
|
||||
|
@ -364,12 +365,15 @@ export const getAvailableVersionsHandler: RequestHandler = async (context, reque
|
|||
}
|
||||
};
|
||||
|
||||
export const getActionStatusHandler: RequestHandler = async (context, request, response) => {
|
||||
export const getActionStatusHandler: RequestHandler<
|
||||
undefined,
|
||||
TypeOf<typeof GetActionStatusRequestSchema.query>
|
||||
> = async (context, request, response) => {
|
||||
const coreContext = await context.core;
|
||||
const esClient = coreContext.elasticsearch.client.asInternalUser;
|
||||
|
||||
try {
|
||||
const actionStatuses = await AgentService.getActionStatuses(esClient);
|
||||
const actionStatuses = await AgentService.getActionStatuses(esClient, request.query);
|
||||
const body: GetActionStatusResponse = { items: actionStatuses };
|
||||
return response.ok({ body });
|
||||
} catch (error) {
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
PostAgentUpgradeRequestSchema,
|
||||
PostBulkAgentUpgradeRequestSchema,
|
||||
PostCancelActionRequestSchema,
|
||||
GetActionStatusRequestSchema,
|
||||
} from '../../types';
|
||||
import * as AgentService from '../../services/agents';
|
||||
import type { FleetConfigType } from '../..';
|
||||
|
@ -248,7 +249,7 @@ export const registerAPIRoutes = (router: FleetAuthzRouter, config: FleetConfigT
|
|||
router.get(
|
||||
{
|
||||
path: AGENT_API_ROUTES.ACTION_STATUS_PATTERN,
|
||||
validate: false,
|
||||
validate: GetActionStatusRequestSchema,
|
||||
fleetAuthz: {
|
||||
fleet: { all: true },
|
||||
},
|
||||
|
|
|
@ -4,60 +4,122 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ElasticsearchClient } from '@kbn/core/server';
|
||||
import pMap from 'p-map';
|
||||
|
||||
import { SO_SEARCH_LIMIT } from '../../constants';
|
||||
|
||||
import type { FleetServerAgentAction, ActionStatus } from '../../types';
|
||||
import type { FleetServerAgentAction, ActionStatus, ListWithKuery } from '../../types';
|
||||
import { AGENT_ACTIONS_INDEX, AGENT_ACTIONS_RESULTS_INDEX } from '../../../common';
|
||||
import { appContextService } from '..';
|
||||
|
||||
/**
|
||||
* Return current bulk actions
|
||||
*/
|
||||
export async function getActionStatuses(esClient: ElasticsearchClient): Promise<ActionStatus[]> {
|
||||
let actions = await _getActions(esClient);
|
||||
const cancelledActionIds = await _getCancelledActionId(esClient);
|
||||
export async function getActionStatuses(
|
||||
esClient: ElasticsearchClient,
|
||||
options: ListWithKuery
|
||||
): Promise<ActionStatus[]> {
|
||||
const actions = await _getActions(esClient, options);
|
||||
const cancelledActions = await _getCancelledActions(esClient);
|
||||
let acks: any;
|
||||
|
||||
// Fetch acknowledged result for every action
|
||||
actions = await pMap(
|
||||
actions,
|
||||
async (action) => {
|
||||
const { count } = await esClient.count({
|
||||
try {
|
||||
acks = await esClient.search({
|
||||
index: AGENT_ACTIONS_RESULTS_INDEX,
|
||||
query: {
|
||||
bool: {
|
||||
// There's some perf/caching advantages to using filter over must
|
||||
// See https://www.elastic.co/guide/en/elasticsearch/reference/current/query-filter-context.html#filter-context
|
||||
filter: [{ terms: { action_id: actions.map((a) => a.actionId) } }],
|
||||
},
|
||||
},
|
||||
size: 0,
|
||||
aggs: {
|
||||
ack_counts: {
|
||||
terms: { field: 'action_id' },
|
||||
aggs: {
|
||||
max_timestamp: { max: { field: '@timestamp' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.statusCode === 404) {
|
||||
// .fleet-actions-results does not yet exist
|
||||
appContextService.getLogger().debug(err);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const action of actions) {
|
||||
const matchingBucket = (acks?.aggregations?.ack_counts as any)?.buckets?.find(
|
||||
(bucket: any) => bucket.key === action.actionId
|
||||
);
|
||||
const nbAgentsAck = matchingBucket?.doc_count ?? 0;
|
||||
const completionTime = (matchingBucket?.max_timestamp as any)?.value_as_string;
|
||||
const nbAgentsActioned = action.nbAgentsActioned || action.nbAgentsActionCreated;
|
||||
const complete = nbAgentsAck === nbAgentsActioned;
|
||||
const cancelledAction = cancelledActions.find((a) => a.actionId === action.actionId);
|
||||
|
||||
let errorCount = 0;
|
||||
try {
|
||||
// query to find errors in action results, cannot do aggregation on text type
|
||||
const res = await esClient.search({
|
||||
index: AGENT_ACTIONS_RESULTS_INDEX,
|
||||
ignore_unavailable: true,
|
||||
track_total_hits: true,
|
||||
rest_total_hits_as_int: true,
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
must: [{ term: { action_id: action.actionId } }],
|
||||
should: [
|
||||
{
|
||||
term: {
|
||||
action_id: action.actionId,
|
||||
exists: {
|
||||
field: 'error',
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
size: 0,
|
||||
});
|
||||
errorCount = (res.hits.total as number) ?? 0;
|
||||
} catch (err) {
|
||||
if (err.statusCode === 404) {
|
||||
// .fleet-actions-results does not yet exist
|
||||
appContextService.getLogger().debug(err);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const nbAgentsActioned = action.nbAgentsActioned || action.nbAgentsActionCreated;
|
||||
const complete = count === nbAgentsActioned;
|
||||
const isCancelled = cancelledActionIds.indexOf(action.actionId) > -1;
|
||||
results.push({
|
||||
...action,
|
||||
nbAgentsAck: nbAgentsAck - errorCount,
|
||||
nbAgentsFailed: errorCount,
|
||||
status:
|
||||
errorCount > 0
|
||||
? 'FAILED'
|
||||
: complete
|
||||
? 'COMPLETE'
|
||||
: cancelledAction
|
||||
? 'CANCELLED'
|
||||
: action.status,
|
||||
nbAgentsActioned,
|
||||
cancellationTime: cancelledAction?.timestamp,
|
||||
completionTime: complete ? completionTime : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...action,
|
||||
nbAgentsAck: count,
|
||||
status: complete ? 'complete' : isCancelled ? 'cancelled' : action.status,
|
||||
nbAgentsActioned,
|
||||
};
|
||||
},
|
||||
{ concurrency: 20 }
|
||||
);
|
||||
|
||||
return actions;
|
||||
return results;
|
||||
}
|
||||
|
||||
async function _getCancelledActionId(esClient: ElasticsearchClient) {
|
||||
async function _getCancelledActions(
|
||||
esClient: ElasticsearchClient
|
||||
): Promise<Array<{ actionId: string; timestamp?: string }>> {
|
||||
const res = await esClient.search<FleetServerAgentAction>({
|
||||
index: AGENT_ACTIONS_INDEX,
|
||||
ignore_unavailable: true,
|
||||
|
@ -70,24 +132,26 @@ async function _getCancelledActionId(esClient: ElasticsearchClient) {
|
|||
type: 'CANCEL',
|
||||
},
|
||||
},
|
||||
{
|
||||
exists: {
|
||||
field: 'agents',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return res.hits.hits.map((hit) => hit._source?.data?.target_id as string);
|
||||
return res.hits.hits.map((hit) => ({
|
||||
actionId: hit._source?.data?.target_id as string,
|
||||
timestamp: hit._source?.['@timestamp'],
|
||||
}));
|
||||
}
|
||||
|
||||
async function _getActions(esClient: ElasticsearchClient) {
|
||||
async function _getActions(
|
||||
esClient: ElasticsearchClient,
|
||||
options: ListWithKuery
|
||||
): Promise<ActionStatus[]> {
|
||||
const res = await esClient.search<FleetServerAgentAction>({
|
||||
index: AGENT_ACTIONS_INDEX,
|
||||
ignore_unavailable: true,
|
||||
size: SO_SEARCH_LIMIT,
|
||||
from: options.page ?? 0,
|
||||
size: options.perPage ?? 20,
|
||||
query: {
|
||||
bool: {
|
||||
must_not: [
|
||||
|
@ -117,20 +181,23 @@ async function _getActions(esClient: ElasticsearchClient) {
|
|||
return acc;
|
||||
}
|
||||
|
||||
if (!acc[hit._source.action_id]) {
|
||||
const startTime = hit._source?.start_time ?? hit._source?.['@timestamp'];
|
||||
const isExpired = hit._source?.expiration
|
||||
? Date.parse(hit._source?.expiration) < Date.now()
|
||||
: false;
|
||||
const source = hit._source!;
|
||||
|
||||
if (!acc[source.action_id!]) {
|
||||
const isExpired = source.expiration ? Date.parse(source.expiration) < Date.now() : false;
|
||||
acc[hit._source.action_id] = {
|
||||
actionId: hit._source.action_id,
|
||||
nbAgentsActionCreated: 0,
|
||||
nbAgentsAck: 0,
|
||||
version: hit._source.data?.version as string,
|
||||
startTime,
|
||||
type: hit._source?.type,
|
||||
nbAgentsActioned: hit._source?.total ?? 0,
|
||||
status: isExpired ? 'expired' : 'in progress',
|
||||
startTime: source.start_time,
|
||||
type: source.type,
|
||||
nbAgentsActioned: source.total ?? 0,
|
||||
status: isExpired ? 'EXPIRED' : 'IN_PROGRESS',
|
||||
expiration: source.expiration,
|
||||
newPolicyId: source.data?.policy_id as string,
|
||||
creationTime: source['@timestamp']!,
|
||||
nbAgentsFailed: 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -49,6 +49,9 @@ export async function reassignAgent(
|
|||
agents: [agentId],
|
||||
created_at: new Date().toISOString(),
|
||||
type: 'POLICY_REASSIGN',
|
||||
data: {
|
||||
policy_id: newAgentPolicyId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -102,6 +102,9 @@ export async function reassignBatch(
|
|||
created_at: now,
|
||||
type: 'POLICY_REASSIGN',
|
||||
total: options.total,
|
||||
data: {
|
||||
policy_id: options.newAgentPolicyId,
|
||||
},
|
||||
});
|
||||
|
||||
return result;
|
||||
|
|
|
@ -159,3 +159,11 @@ export const GetAgentDataRequestSchema = {
|
|||
previewData: schema.boolean({ defaultValue: false }),
|
||||
}),
|
||||
};
|
||||
|
||||
export const GetActionStatusRequestSchema = {
|
||||
query: schema.object({
|
||||
page: schema.number({ defaultValue: 0 }),
|
||||
perPage: schema.number({ defaultValue: 20 }),
|
||||
kuery: schema.maybe(schema.string()),
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -12418,10 +12418,7 @@
|
|||
"xpack.fleet.createPackagePolicy.stepConfigure.packagePolicyNamespaceHelpLabel": "Modifiez l'espace de nom par défaut hérité de la stratégie d'agent sélectionnée. Ce paramètre modifie le nom du flux de données de l'intégration. {learnMore}.",
|
||||
"xpack.fleet.createPackagePolicy.stepConfigure.showStreamsAriaLabel": "Afficher les entrées {type}",
|
||||
"xpack.fleet.createPackagePolicy.StepSelectPolicy.agentPolicyAgentsDescriptionText": "{count, plural, one {# agent est enregistré} other {# agents sont enregistrés}} avec la stratégie d'agent sélectionnée.",
|
||||
"xpack.fleet.currentUpgrade.calloutDescription": "Pour en savoir plus, consultez le {guideLink}.",
|
||||
"xpack.fleet.currentUpgrade.calloutTitle": "Mise à niveau de {nbAgents, plural, one {# agent} other {# agents}} vers la version {version}",
|
||||
"xpack.fleet.currentUpgrade.confirmDescription": "Cette action provoquera l'abandon de la mise à niveau de {nbAgents} agents",
|
||||
"xpack.fleet.currentUpgrade.scheduleCalloutTitle": "{nbAgents} agents programmés pour la mise à niveau vers la version {version} le {date}",
|
||||
"xpack.fleet.debug.agentPolicyDebugger.description": "Recherchez une stratégie d'agent par son nom ou sa valeur {codeId}. Utilisez le bloc de code ci-dessous pour diagnostiquer tout problème potentiel dans la configuration de la stratégie.",
|
||||
"xpack.fleet.debug.dangerZone.description": "Cette page fournit une interface pour traiter directement les problèmes de données sous-jacentes de Fleet et diagnostiquer les problèmes. Notez que ces outils de débogage peuvent être par nature {strongDestructive} et déboucher sur une {strongLossOfData}. Agissez avec prudence.",
|
||||
"xpack.fleet.debug.integrationDebugger.reinstall.error": "Erreur lors de la réinstallation de {integrationTitle}",
|
||||
|
@ -12902,10 +12899,7 @@
|
|||
"xpack.fleet.createPackagePolicyBottomBar.backButton": "Retour",
|
||||
"xpack.fleet.createPackagePolicyBottomBar.loading": "Chargement...",
|
||||
"xpack.fleet.currentUpgrade.abortRequestError": "Une erreur s'est produite lors de l'abandon de la mise à niveau",
|
||||
"xpack.fleet.currentUpgrade.abortUpgradeButtom": "Abandonner la mise à niveau",
|
||||
"xpack.fleet.currentUpgrade.confirmTitle": "Abandonner la mise à niveau ?",
|
||||
"xpack.fleet.currentUpgrade.fetchRequestError": "Une erreur s'est produite lors de la récupération des mises à niveau actuelles",
|
||||
"xpack.fleet.currentUpgrade.guideLink": "Guide de Fleet et d'Elastic Agent",
|
||||
"xpack.fleet.dataStreamList.actionsColumnTitle": "Actions",
|
||||
"xpack.fleet.dataStreamList.datasetColumnTitle": "Ensemble de données",
|
||||
"xpack.fleet.dataStreamList.integrationColumnTitle": "Intégration",
|
||||
|
|
|
@ -12405,10 +12405,7 @@
|
|||
"xpack.fleet.createPackagePolicy.stepConfigure.packagePolicyNamespaceHelpLabel": "選択したエージェントポリシーから継承されたデフォルト名前空間を変更します。この設定により、統合のデータストリームの名前が変更されます。{learnMore}。",
|
||||
"xpack.fleet.createPackagePolicy.stepConfigure.showStreamsAriaLabel": "{type}入力を表示",
|
||||
"xpack.fleet.createPackagePolicy.StepSelectPolicy.agentPolicyAgentsDescriptionText": "{count, plural, other {# 個のエージェント}}が選択したエージェントポリシーで登録されています。",
|
||||
"xpack.fleet.currentUpgrade.calloutDescription": "詳細については、{guideLink}を参照してください。",
|
||||
"xpack.fleet.currentUpgrade.calloutTitle": "{nbAgents, plural, other {# 個のエージェント}}をバージョン{version}にアップグレードしています",
|
||||
"xpack.fleet.currentUpgrade.confirmDescription": "{nbAgents}個のエージェントのアップグレードが中断されます",
|
||||
"xpack.fleet.currentUpgrade.scheduleCalloutTitle": "{nbAgents}個のエージェントが{date}にバージョン{version}にアップグレードされるようにスケジュールされました",
|
||||
"xpack.fleet.debug.agentPolicyDebugger.description": "名前または{codeId}値を使用して、エージェントポリシーを検索します。以下のコードブロックを使用して、ポリシーの構成に関する潜在的な問題を診断します。",
|
||||
"xpack.fleet.debug.dangerZone.description": "このページは、Fleetの基本データと診断の問題を直接管理するためのインターフェイスです。これらのデバッグツールは、本質的に{strongDestructive}である可能性があり、{strongLossOfData}のおそれがあります。十分にご注意ください。",
|
||||
"xpack.fleet.debug.integrationDebugger.reinstall.error": "{integrationTitle}の再インストールエラー",
|
||||
|
@ -12890,10 +12887,7 @@
|
|||
"xpack.fleet.createPackagePolicyBottomBar.backButton": "戻る",
|
||||
"xpack.fleet.createPackagePolicyBottomBar.loading": "読み込み中...",
|
||||
"xpack.fleet.currentUpgrade.abortRequestError": "アップグレードの中断中にエラーが発生しました",
|
||||
"xpack.fleet.currentUpgrade.abortUpgradeButtom": "アップグレードの中断",
|
||||
"xpack.fleet.currentUpgrade.confirmTitle": "アップグレードを中断しますか?",
|
||||
"xpack.fleet.currentUpgrade.fetchRequestError": "最新のアップグレードの取得中にエラーが発生しました",
|
||||
"xpack.fleet.currentUpgrade.guideLink": "FleetおよびElasticエージェントガイド",
|
||||
"xpack.fleet.dataStreamList.actionsColumnTitle": "アクション",
|
||||
"xpack.fleet.dataStreamList.datasetColumnTitle": "データセット",
|
||||
"xpack.fleet.dataStreamList.integrationColumnTitle": "統合",
|
||||
|
|
|
@ -12421,10 +12421,7 @@
|
|||
"xpack.fleet.createPackagePolicy.stepConfigure.packagePolicyNamespaceHelpLabel": "更改从选定代理策略继承的默认命名空间。此设置将更改集成的数据流的名称。{learnMore}。",
|
||||
"xpack.fleet.createPackagePolicy.stepConfigure.showStreamsAriaLabel": "显示 {type} 输入",
|
||||
"xpack.fleet.createPackagePolicy.StepSelectPolicy.agentPolicyAgentsDescriptionText": "{count, plural, other {# 个代理}}已注册到选定代理策略。",
|
||||
"xpack.fleet.currentUpgrade.calloutDescription": "有关详细信息,请参阅 {guideLink}。",
|
||||
"xpack.fleet.currentUpgrade.calloutTitle": "正在将 {nbAgents, plural, other {# 个代理}}升级到版本 {version}",
|
||||
"xpack.fleet.currentUpgrade.confirmDescription": "此操作会中止升级 {nbAgents} 个代理",
|
||||
"xpack.fleet.currentUpgrade.scheduleCalloutTitle": "{nbAgents} 个代理计划于 {date}升级到版本 {version}",
|
||||
"xpack.fleet.debug.agentPolicyDebugger.description": "使用其名称或 {codeId} 值搜索代理策略。使用以下代码块诊断策略配置出现的任何潜在问题。",
|
||||
"xpack.fleet.debug.dangerZone.description": "此页面提供了一个界面,用于直接管理 Fleet 的底层数据并诊断问题。请注意,这些故障排查工具在本质上可能{strongDestructive},并可能导致{strongLossOfData}。请谨慎操作。",
|
||||
"xpack.fleet.debug.integrationDebugger.reinstall.error": "重新安装 {integrationTitle} 时出错",
|
||||
|
@ -12907,10 +12904,7 @@
|
|||
"xpack.fleet.createPackagePolicyBottomBar.backButton": "返回",
|
||||
"xpack.fleet.createPackagePolicyBottomBar.loading": "正在加载……",
|
||||
"xpack.fleet.currentUpgrade.abortRequestError": "中止升级时发生错误",
|
||||
"xpack.fleet.currentUpgrade.abortUpgradeButtom": "中止升级",
|
||||
"xpack.fleet.currentUpgrade.confirmTitle": "是否中止升级?",
|
||||
"xpack.fleet.currentUpgrade.fetchRequestError": "提取当前升级时出错",
|
||||
"xpack.fleet.currentUpgrade.guideLink": "Fleet 和 Elastic 代理指南",
|
||||
"xpack.fleet.dataStreamList.actionsColumnTitle": "操作",
|
||||
"xpack.fleet.dataStreamList.datasetColumnTitle": "数据集",
|
||||
"xpack.fleet.dataStreamList.integrationColumnTitle": "集成",
|
||||
|
|
|
@ -223,7 +223,7 @@ export default function (providerContext: FtrProviderContext) {
|
|||
body: { items: actionStatuses },
|
||||
} = await supertest.get(`/api/fleet/agents/action_status`).set('kbn-xsrf', 'xxx');
|
||||
|
||||
const action = actionStatuses.find((a: any) => a.actionId === actionId);
|
||||
const action = actionStatuses?.find((a: any) => a.actionId === actionId);
|
||||
if (action && action.nbAgentsActioned === action.nbAgentsActionCreated) {
|
||||
clearInterval(intervalId);
|
||||
resolve({});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue