Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
c23db52
moving action batching to async
juliaElastic Aug 16, 2022
6cdbf4e
created new api for action status
juliaElastic Aug 16, 2022
d9da608
fix types
juliaElastic Aug 16, 2022
d8e968f
fix types
juliaElastic Aug 16, 2022
e7fd443
added time measurement
juliaElastic Aug 16, 2022
8e5667a
added action status component, refactored reassign logic
juliaElastic Aug 17, 2022
3c94459
refactored retry task to be reusable by other actions
juliaElastic Aug 17, 2022
8a31e6c
refactored action runner to class
juliaElastic Aug 18, 2022
8d705a6
changed bulk update tags
juliaElastic Aug 22, 2022
8d692d2
Merge branch 'main' into feat/action-batch-async
juliaElastic Aug 22, 2022
8da6894
build fixes
juliaElastic Aug 22, 2022
1afb694
fix checks
juliaElastic Aug 22, 2022
1137591
passing retry options
juliaElastic Aug 22, 2022
6de094e
fixed action status
juliaElastic Aug 23, 2022
07f3283
improvements to action status api
juliaElastic Aug 23, 2022
83fe4b6
improvements to action status api
juliaElastic Aug 23, 2022
ec22cbc
removed timed out status
juliaElastic Aug 23, 2022
ddca370
simplified code around retry tasks
juliaElastic Aug 23, 2022
0fb109c
added missing unenroll params
juliaElastic Aug 23, 2022
a117ba3
Merge branch 'main' into feat/action-batch-async
juliaElastic Aug 24, 2022
dab606f
fix checks
juliaElastic Aug 24, 2022
f4653e2
increased upgrade action expiration to 2h for immediately
juliaElastic Aug 24, 2022
e38afd0
moved getActionStatusHandler to handlers, using its own response type
juliaElastic Aug 24, 2022
edd2ab5
capturing failures with error message in a new .fleet-actions-status …
juliaElastic Aug 24, 2022
0bf645a
renamed status index so it doesn't get the same alias as .fleet-actions
juliaElastic Aug 24, 2022
88a6362
cleaned up action and retry params
juliaElastic Aug 25, 2022
a59df36
refactored to simplify passing params
juliaElastic Aug 26, 2022
92db446
Merge branch 'main' into feat/action-batch-async
kibanamachine Aug 26, 2022
53c55c4
added script to generate agent docs, fixed bugs
juliaElastic Aug 26, 2022
dc10593
renamed action_status response props
juliaElastic Aug 26, 2022
76a87fd
Merge branch 'main' into feat/action-batch-async
kibanamachine Aug 29, 2022
abea983
Merge branch 'main' into feat/action-batch-async
juliaElastic Aug 30, 2022
32579ff
Merge branch 'main' into feat/action-batch-async
juliaElastic Sep 5, 2022
6d73425
Merge branch 'main' into feat/action-batch-async
juliaElastic Sep 5, 2022
6c81a64
small cleanup
juliaElastic Sep 5, 2022
6978057
refactor, cleanup
juliaElastic Sep 5, 2022
f02b268
added tests
juliaElastic Sep 6, 2022
5525dc1
fixed tests
juliaElastic Sep 7, 2022
97b1edd
upgrade api test
juliaElastic Sep 7, 2022
951566c
Merge branch 'main' into feat/action-batch-async
kibanamachine Sep 7, 2022
f1eb373
renamed CurrentAction to ActionStatus model object
juliaElastic Sep 7, 2022
be50c8f
moved out action status UI and .fleet-action-status changes to simpli…
juliaElastic Sep 7, 2022
c0b02d7
fixed toast message for single agent actions
juliaElastic Sep 8, 2022
f26a1f5
added comments
juliaElastic Sep 8, 2022
9e3c01c
Merge branch 'main' into feat/action-batch-async
juliaElastic Sep 9, 2022
fa9bb47
fixed conflict resolution
juliaElastic Sep 9, 2022
7e95311
reverted refreshAgents change
juliaElastic Sep 9, 2022
1d7643b
Merge branch 'main' into feat/action-batch-async
kibanamachine Sep 12, 2022
921e96c
review comments
juliaElastic Sep 12, 2022
c303407
Merge branch 'main' into feat/action-batch-async
kibanamachine Sep 12, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions x-pack/plugins/fleet/common/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export const AGENT_API_ROUTES = {
UPGRADE_PATTERN: `${API_ROOT}/agents/{agentId}/upgrade`,
BULK_UPGRADE_PATTERN: `${API_ROOT}/agents/bulk_upgrade`,
CURRENT_UPGRADES_PATTERN: `${API_ROOT}/agents/current_upgrades`,
ACTION_STATUS_PATTERN: `${API_ROOT}/agents/action_status`,
LIST_TAGS_PATTERN: `${API_ROOT}/agents/tags`,
};

Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/fleet/common/services/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ export const agentRouteService = {
getUpgradePath: (agentId: string) =>
AGENT_API_ROUTES.UPGRADE_PATTERN.replace('{agentId}', agentId),
getBulkUpgradePath: () => AGENT_API_ROUTES.BULK_UPGRADE_PATTERN,
getActionStatusPath: () => AGENT_API_ROUTES.ACTION_STATUS_PATTERN,
getCurrentUpgradesPath: () => AGENT_API_ROUTES.CURRENT_UPGRADES_PATTERN,
getCancelActionPath: (actionId: string) =>
AGENT_API_ROUTES.CANCEL_ACTIONS_PATTERN.replace('{actionId}', actionId),
Expand Down
16 changes: 16 additions & 0 deletions x-pack/plugins/fleet/common/types/models/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export interface NewAgentAction {
start_time?: string;
minimum_execution_duration?: number;
source_uri?: string;
total?: number;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the total count that represents how many agents were actioned (clicked by user), this helps with status reporting in case something went wrong while creating the action documents in batches.

}

export interface AgentAction extends NewAgentAction {
Expand Down Expand Up @@ -105,6 +106,19 @@ export interface CurrentUpgrade {
startTime?: string;
}

export interface CurrentAction {
actionId: string;
complete: boolean;
nbAgents: number;
nbAgentsAck: number;
version: string;
startTime?: string;
type?: string;
total: number;
timedOut: boolean;
failed: number;
}

// Generated from FleetServer schema.json

/**
Expand Down Expand Up @@ -284,5 +298,7 @@ export interface FleetServerAgentAction {
data?: {
[k: string]: unknown;
};

total?: number;
[k: string]: unknown;
}
6 changes: 5 additions & 1 deletion x-pack/plugins/fleet/common/types/rest_spec/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import type { SearchHit } from '@kbn/core/types/elasticsearch';

import type { Agent, AgentAction, CurrentUpgrade, NewAgentAction } from '../models';
import type { Agent, AgentAction, CurrentAction, CurrentUpgrade, NewAgentAction } from '../models';

import type { ListResult, ListWithKuery } from './common';

Expand Down Expand Up @@ -125,6 +125,7 @@ export interface PostBulkAgentReassignRequest {
body: {
policy_id: string;
agents: string[] | string;
batchSize?: number;
};
}

Expand Down Expand Up @@ -205,6 +206,9 @@ export interface GetAgentIncomingDataResponse {
export interface GetCurrentUpgradesResponse {
items: CurrentUpgrade[];
}
export interface GetActionStatusResponse {
items: CurrentAction[];
}
export interface GetAvailableVersionsResponse {
items: string[];
}
2 changes: 1 addition & 1 deletion x-pack/plugins/fleet/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"server": true,
"ui": true,
"configPath": ["xpack", "fleet"],
"requiredPlugins": ["licensing", "data", "encryptedSavedObjects", "navigation", "customIntegrations", "share", "spaces", "security", "unifiedSearch", "savedObjectsTagging"],
"requiredPlugins": ["licensing", "data", "encryptedSavedObjects", "navigation", "customIntegrations", "share", "spaces", "security", "unifiedSearch", "savedObjectsTagging", "taskManager"],
"optionalPlugins": ["features", "cloud", "usageCollection", "home", "globalSearch", "telemetry", "discover", "ingestPipelines"],
"extraPublicDirs": ["common"],
"requiredBundles": ["kibanaReact", "cloud", "esUiShared", "infra", "kibanaUtils", "usageCollection", "unifiedSearch"]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* 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, { useEffect } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiLoadingSpinner,
EuiIcon,
} from '@elastic/eui';

import type { CurrentAction } from '../../../../types';
import { useActionStatus } from '../hooks';

export const ActionStatusCallout: React.FunctionComponent<{ refreshActionStatus: boolean }> = ({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the ActionStatusCallout from agent list, as this is going to move to a flyout. Will raise a new pr with the UI changes.

refreshActionStatus,
}) => {
const { currentActions, refreshActions } = useActionStatus();

useEffect(() => {
setTimeout(refreshActions, 10000);
}, [refreshActionStatus, refreshActions]);

const actionNames: { [key: string]: string } = {
POLICY_REASSIGN: 'Reassign',
UPGRADE: 'Upgrade',
UNENROLL: 'Unenroll',
CANCEL: 'Cancel',
ACTION: 'Action',
};

const calloutTitle = (currentAction: CurrentAction) => (
<FormattedMessage
id="xpack.fleet.currentAction.calloutTitle"
defaultMessage="{type} {status}, {total} actioned, {nbAgentsAck} acknowledged, {failed} failed, actionId: {actionId}"
values={{
status: currentAction.complete
? 'completed'
: currentAction.timedOut
? 'timed out'
: currentAction.failed > 0
? 'failed'
: 'in progress',
type: actionNames[currentAction.type ?? 'ACTION'],
total: currentAction.total,
nbAgents: currentAction.nbAgents,
nbAgentsAck: currentAction.nbAgentsAck,
actionId: currentAction.actionId,
failed: currentAction.failed,
}}
/>
);
return (
<>
{currentActions
.filter((action) => action.type !== 'UPGRADE')
.slice(0, 3)
.map((currentAction) => (
<React.Fragment key={currentAction.actionId}>
<EuiCallOut
color={
currentAction.complete
? 'success'
: currentAction.timedOut || currentAction.failed > 0
? 'danger'
: 'primary'
}
>
<EuiFlexGroup
className="euiCallOutHeader__title"
justifyContent="spaceBetween"
alignItems="center"
gutterSize="none"
>
<EuiFlexItem grow={false}>
<div>
{!currentAction.complete && !currentAction.timedOut ? (
<EuiLoadingSpinner />
) : currentAction.complete ? (
<EuiIcon type="check" />
) : (
<EuiIcon type="alert" />
)}
&nbsp;&nbsp;
{calloutTitle(currentAction)}
</div>
</EuiFlexItem>
</EuiFlexGroup>
</EuiCallOut>
<EuiSpacer size="l" />
</React.Fragment>
))}
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@

export { CurrentBulkUpgradeCallout } from './current_bulk_upgrade_callout';
export type { CurrentBulkUpgradeCalloutProps } from './current_bulk_upgrade_callout';
export { ActionStatusCallout } from './action_status';
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@

export { useCurrentUpgrades } from './use_current_upgrades';
export { useUpdateTags } from './use_update_tags';
export { useActionStatus } from './use_action_status';
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { useCallback, useEffect, useRef, useState } from 'react';
import { i18n } from '@kbn/i18n';

import { sendGetActionStatus, useStartServices } from '../../../../hooks';

import type { CurrentAction } from '../../../../types';

const POLL_INTERVAL = 30 * 1000;

export function useActionStatus() {
const [currentActions, setCurrentActions] = useState<CurrentAction[]>([]);
const currentTimeoutRef = useRef<NodeJS.Timeout>();
const isCancelledRef = useRef<boolean>(false);
const { notifications } = useStartServices();

const refreshActions = useCallback(async () => {
try {
const res = await sendGetActionStatus();
if (isCancelledRef.current) {
return;
}
if (res.error) {
throw res.error;
}

if (!res.data) {
throw new Error('No data');
}

setCurrentActions(res.data.items);
} catch (err) {
notifications.toasts.addError(err, {
title: i18n.translate('xpack.fleet.actionStatus.fetchRequestError', {
defaultMessage: 'An error happened while fetching action status',
}),
});
}
}, [notifications.toasts]);

// Poll for upgrades
useEffect(() => {
isCancelledRef.current = false;

async function pollData() {
await refreshActions();
if (isCancelledRef.current) {
return;
}
currentTimeoutRef.current = setTimeout(() => pollData(), POLL_INTERVAL);
}

pollData();

return () => {
isCancelledRef.current = true;

if (currentTimeoutRef.current) {
clearTimeout(currentTimeoutRef.current);
}
};
}, [refreshActions]);

return {
currentActions,
refreshActions,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import {
} from '../components';
import { useFleetServerUnhealthy } from '../hooks/use_fleet_server_unhealthy';

import { CurrentBulkUpgradeCallout } from './components';
import { ActionStatusCallout, CurrentBulkUpgradeCallout } from './components';
import { AgentTableHeader } from './components/table_header';
import type { SelectionMode } from './components/types';
import { SearchAndFilterBar } from './components/search_and_filter_bar';
Expand Down Expand Up @@ -404,6 +404,8 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
// Current upgrades
const { abortUpgrade, currentUpgrades, refreshUpgrades } = useCurrentUpgrades(fetchData);

const [refreshActionStatus, setRefreshActionStatus] = useState(false);

const columns = [
{
field: HOSTNAME_FIELD,
Expand Down Expand Up @@ -626,6 +628,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
<EuiSpacer size="l" />
</React.Fragment>
))}
<ActionStatusCallout refreshActionStatus={refreshActionStatus} />
{/* Search and filter bar */}
<SearchAndFilterBar
agentPolicies={agentPolicies}
Expand All @@ -646,9 +649,10 @@ 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 } = {}) => {
setRefreshActionStatus(!refreshActionStatus);
Promise.all([fetchData({ refreshTags }), refreshUpgrades()]);
}}
onClickAddAgent={() => setEnrollmentFlyoutState({ isOpen: true })}
onClickAddFleetServer={onClickAddFleetServer}
visibleAgents={agents}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,20 @@ export const AgentReassignAgentPolicyModal: React.FunctionComponent<Props> = ({
throw res.error;
}
setIsSubmitting(false);
const hasCompleted = Object.keys(res.data ?? {}).length > 0;
const successMessage = i18n.translate(
'xpack.fleet.agentReassignPolicy.successSingleNotificationTitle',
{
defaultMessage: 'Agent policy reassigned',
}
);
notifications.toasts.addSuccess(successMessage);
const submittedMessage = i18n.translate(
'xpack.fleet.agentReassignPolicy.submittedNotificationTitle',
{
defaultMessage: 'Agent policy reassign submitted',
}
);
notifications.toasts.addSuccess(hasCompleted ? successMessage : submittedMessage);
onClose();
} catch (error) {
setIsSubmitting(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const AgentUnenrollAgentModal: React.FunctionComponent<Props> = ({
async function onSubmit() {
try {
setIsSubmitting(true);
const { error } = isSingleAgent
const { error, data } = isSingleAgent
? await sendPostAgentUnenroll((agents[0] as Agent).id, {
revoke: forceUnenroll,
})
Expand All @@ -52,6 +52,13 @@ export const AgentUnenrollAgentModal: React.FunctionComponent<Props> = ({
throw error;
}
setIsSubmitting(false);
const hasCompleted = Object.keys(data ?? {}).length > 0;
const submittedMessage = i18n.translate(
'xpack.fleet.unenrollAgents.submittedNotificationTitle',
{
defaultMessage: 'Agent(s) unenroll submitted',
}
);
if (forceUnenroll) {
const successMessage = isSingleAgent
? i18n.translate('xpack.fleet.unenrollAgents.successForceSingleNotificationTitle', {
Expand All @@ -60,7 +67,7 @@ export const AgentUnenrollAgentModal: React.FunctionComponent<Props> = ({
: i18n.translate('xpack.fleet.unenrollAgents.successForceMultiNotificationTitle', {
defaultMessage: 'Agents unenrolled',
});
notifications.toasts.addSuccess(successMessage);
notifications.toasts.addSuccess(hasCompleted ? successMessage : submittedMessage);
} else {
const successMessage = isSingleAgent
? i18n.translate('xpack.fleet.unenrollAgents.successSingleNotificationTitle', {
Expand All @@ -69,7 +76,7 @@ export const AgentUnenrollAgentModal: React.FunctionComponent<Props> = ({
: i18n.translate('xpack.fleet.unenrollAgents.successMultiNotificationTitle', {
defaultMessage: 'Unenrolling agents',
});
notifications.toasts.addSuccess(successMessage);
notifications.toasts.addSuccess(hasCompleted ? successMessage : submittedMessage);
}
onClose();
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,18 @@ export const AgentUpgradeAgentModal: React.FunctionComponent<AgentUpgradeAgentMo
);
setIsSubmitting(false);

const hasCompleted = Object.keys(data ?? {}).length > 0;
const submittedMessage = i18n.translate(
'xpack.fleet.upgradeAgents.submittedNotificationTitle',
{
defaultMessage: 'Agent(s) upgrade submitted',
}
);

if (!hasCompleted) {
notifications.toasts.addSuccess(submittedMessage);
}

if (isSingleAgent && counts.success === counts.total) {
notifications.toasts.addSuccess(
i18n.translate('xpack.fleet.upgradeAgents.successSingleNotificationTitle', {
Expand Down
Loading