[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:
Julia Bardi 2022-09-15 15:28:27 +02:00 committed by GitHub
parent 6aae2e1db8
commit 1709be2d27
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 1138 additions and 270 deletions

View file

@ -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": [
{

View file

@ -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:

View file

@ -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':

View file

@ -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

View file

@ -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

View file

@ -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>
);
};

View file

@ -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" />
&nbsp;
<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."
/>
&nbsp;
{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>.&nbsp;
</>
) : (
<>{inProgressDescription(action.creationTime)}&nbsp;</>
)}
<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>
);
};

View file

@ -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);
});
});

View file

@ -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;
}

View file

@ -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"
/>
&nbsp;
<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 />}
&nbsp;&nbsp;
{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>
);
};

View file

@ -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';

View file

@ -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>

View file

@ -5,5 +5,5 @@
* 2.0.
*/
export { useCurrentUpgrades } from './use_current_upgrades';
export { useUpdateTags } from './use_update_tags';
export { useActionStatus } from './use_action_status';

View file

@ -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,
};
}

View file

@ -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 */}

View file

@ -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) {

View file

@ -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) {

View file

@ -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 },
},

View file

@ -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,
};
}

View file

@ -49,6 +49,9 @@ export async function reassignAgent(
agents: [agentId],
created_at: new Date().toISOString(),
type: 'POLICY_REASSIGN',
data: {
policy_id: newAgentPolicyId,
},
});
}

View file

@ -102,6 +102,9 @@ export async function reassignBatch(
created_at: now,
type: 'POLICY_REASSIGN',
total: options.total,
data: {
policy_id: options.newAgentPolicyId,
},
});
return result;

View file

@ -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()),
}),
};

View file

@ -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",

View file

@ -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": "統合",

View file

@ -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": "集成",

View file

@ -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({});