From 741907acb8852cf24dae29851bf5cbed5265d7c6 Mon Sep 17 00:00:00 2001
From: Luciano Gorza
Date: Fri, 1 Mar 2024 09:43:26 -0300
Subject: [PATCH 01/11] Add upgrade action
---
.../services/get-outdated-agents.tsx | 14 +++-
.../endpoints-summary/services/index.tsx | 2 +
.../services/upgrade-agents.tsx | 15 ++++
.../table/actions/actions.tsx | 47 +++++++++++
.../table/actions/upgrade-agent-modal.tsx | 79 +++++++++++++++++++
.../endpoints-summary/table/agents-table.tsx | 61 ++++++++++++--
.../endpoints-summary/table/columns.tsx | 19 ++++-
.../components/endpoints-summary/types.ts | 5 ++
8 files changed, 232 insertions(+), 10 deletions(-)
create mode 100644 plugins/main/public/components/endpoints-summary/services/upgrade-agents.tsx
create mode 100644 plugins/main/public/components/endpoints-summary/table/actions/upgrade-agent-modal.tsx
diff --git a/plugins/main/public/components/endpoints-summary/services/get-outdated-agents.tsx b/plugins/main/public/components/endpoints-summary/services/get-outdated-agents.tsx
index cd35c2d442..36e44d173e 100644
--- a/plugins/main/public/components/endpoints-summary/services/get-outdated-agents.tsx
+++ b/plugins/main/public/components/endpoints-summary/services/get-outdated-agents.tsx
@@ -1,10 +1,20 @@
import { WzRequest } from '../../../react-services/wz-request';
-export const getOutdatedAgents = async () => {
+export const getOutdatedAgents = async (agentIds?: string[]) => {
const {
data: {
data: { affected_items },
},
- } = await WzRequest.apiReq('GET', '/agents/outdated', {});
+ } = await WzRequest.apiReq(
+ 'GET',
+ '/agents/outdated',
+ agentIds
+ ? {
+ params: {
+ q: `(${agentIds.map(agentId => `id=${agentId}`).join(',')})`,
+ },
+ }
+ : {},
+ );
return affected_items;
};
diff --git a/plugins/main/public/components/endpoints-summary/services/index.tsx b/plugins/main/public/components/endpoints-summary/services/index.tsx
index a8d7711ff8..cd6d8c0401 100644
--- a/plugins/main/public/components/endpoints-summary/services/index.tsx
+++ b/plugins/main/public/components/endpoints-summary/services/index.tsx
@@ -4,3 +4,5 @@ export { removeAgentsFromGroupService } from './remove-agents-from-group';
export { addAgentToGroupService } from './add-agent-to-group';
export { addAgentsToGroupService } from './add-agents-to-group';
export { getGroupsService } from './get-groups';
+export { upgradeAgentsService } from './upgrade-agents';
+export { getOutdatedAgents } from './get-outdated-agents';
diff --git a/plugins/main/public/components/endpoints-summary/services/upgrade-agents.tsx b/plugins/main/public/components/endpoints-summary/services/upgrade-agents.tsx
new file mode 100644
index 0000000000..ea9c8f7e6b
--- /dev/null
+++ b/plugins/main/public/components/endpoints-summary/services/upgrade-agents.tsx
@@ -0,0 +1,15 @@
+import IApiResponse from '../../../react-services/interfaces/api-response.interface';
+import { WzRequest } from '../../../react-services/wz-request';
+import { ResponseUpgradeAgents } from '../types';
+
+export const upgradeAgentsService = async ({
+ agentIds,
+}: {
+ agentIds: string[];
+}) =>
+ (await WzRequest.apiReq('PUT', `/agents/upgrade`, {
+ params: {
+ agents_list: agentIds.join(','),
+ wait_for_complete: true,
+ },
+ })) as IApiResponse;
diff --git a/plugins/main/public/components/endpoints-summary/table/actions/actions.tsx b/plugins/main/public/components/endpoints-summary/table/actions/actions.tsx
index d6137893e8..1a799c5bef 100644
--- a/plugins/main/public/components/endpoints-summary/table/actions/actions.tsx
+++ b/plugins/main/public/components/endpoints-summary/table/actions/actions.tsx
@@ -10,6 +10,8 @@ export const agentsTableActions = (
allowEditGroups: boolean,
setAgent: (agent: Agent) => void,
setIsEditGroupsVisible: (visible: boolean) => void,
+ setIsUpgradeModalVisible: (visible: boolean) => void,
+ outdatedAgents: Agent[],
) => [
{
name: agent => {
@@ -80,4 +82,49 @@ export const agentsTableActions = (
'data-test-subj': 'action-groups',
enabled: () => allowEditGroups,
},
+ {
+ name: agent => {
+ const name = Upgrade;
+
+ const isOutdated = !!outdatedAgents.find(
+ outdatedAgent => outdatedAgent.id === agent.id,
+ );
+
+ if (agent.status === API_NAME_AGENT_STATUS.ACTIVE && isOutdated) {
+ return (
+
+ {name}
+
+ );
+ }
+
+ return (
+
+ {name}
+
+ );
+ },
+ description: 'Upgrade',
+ icon: 'package',
+ type: 'icon',
+ onClick: agent => {
+ setAgent(agent);
+ setIsUpgradeModalVisible(true);
+ },
+ 'data-test-subj': 'action-upgrade',
+ enabled: agent => {
+ const isOutdated = !!outdatedAgents.find(
+ outdatedAgent => outdatedAgent.id === agent.id,
+ );
+ return agent.status === API_NAME_AGENT_STATUS.ACTIVE && isOutdated;
+ },
+ },
];
diff --git a/plugins/main/public/components/endpoints-summary/table/actions/upgrade-agent-modal.tsx b/plugins/main/public/components/endpoints-summary/table/actions/upgrade-agent-modal.tsx
new file mode 100644
index 0000000000..b7dd113300
--- /dev/null
+++ b/plugins/main/public/components/endpoints-summary/table/actions/upgrade-agent-modal.tsx
@@ -0,0 +1,79 @@
+import React, { useState } from 'react';
+import { EuiConfirmModal } from '@elastic/eui';
+import { compose } from 'redux';
+import { withErrorBoundary, withReduxProvider } from '../../../common/hocs';
+import { UI_LOGGER_LEVELS } from '../../../../../common/constants';
+import { UI_ERROR_SEVERITIES } from '../../../../react-services/error-orchestrator/types';
+import { getErrorOrchestrator } from '../../../../react-services/common-services';
+import { upgradeAgentsService } from '../../services';
+import { Agent } from '../../types';
+import { getToasts } from '../../../../kibana-services';
+
+interface UpgradeAgentModalProps {
+ agent: Agent;
+ onClose: () => void;
+ reloadAgents: () => void;
+}
+
+export const UpgradeAgentModal = compose(
+ withErrorBoundary,
+ withReduxProvider,
+)(({ agent, onClose, reloadAgents }: UpgradeAgentModalProps) => {
+ const [isLoading, setIsLoading] = useState(false);
+ const showToast = (
+ color: string,
+ title: string = '',
+ text: string = '',
+ time: number = 3000,
+ ) => {
+ getToasts().add({
+ color: color,
+ title: title,
+ text: text,
+ toastLifeTimeMs: time,
+ });
+ };
+
+ const handleOnSave = async () => {
+ setIsLoading(true);
+
+ try {
+ await upgradeAgentsService({ agentIds: [agent.id] });
+ showToast('success', 'Upgrade agent', 'Agent successfully upgraded');
+ reloadAgents();
+ } catch (error) {
+ const options = {
+ context: `UpgradeAgentModal.handleOnSave`,
+ level: UI_LOGGER_LEVELS.ERROR,
+ severity: UI_ERROR_SEVERITIES.BUSINESS,
+ store: true,
+ error: {
+ error,
+ message: error.message || error,
+ title: `Could not upgrade agent`,
+ },
+ };
+ getErrorOrchestrator().handleError(options);
+ } finally {
+ setIsLoading(false);
+ onClose();
+ }
+ };
+
+ return (
+ {
+ ev.stopPropagation();
+ }}
+ isLoading={isLoading}
+ >
+ {`Upgrade agent ${agent?.name}?`}
+
+ );
+});
diff --git a/plugins/main/public/components/endpoints-summary/table/agents-table.tsx b/plugins/main/public/components/endpoints-summary/table/agents-table.tsx
index 1823b0e7c1..a782e22a4e 100644
--- a/plugins/main/public/components/endpoints-summary/table/agents-table.tsx
+++ b/plugins/main/public/components/endpoints-summary/table/agents-table.tsx
@@ -25,6 +25,7 @@ import {
UI_ORDER_AGENT_STATUS,
AGENT_SYNCED_STATUS,
SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT,
+ UI_LOGGER_LEVELS,
} from '../../../../common/constants';
import { TableWzAPI } from '../../common/tables';
import { WzRequest } from '../../../react-services/wz-request';
@@ -39,6 +40,10 @@ import { updateCurrentAgentData } from '../../../redux/actions/appStateActions';
import { agentsTableColumns } from './columns';
import { AgentsTableGlobalActions } from './global-actions/global-actions';
import { Agent } from '../types';
+import { UpgradeAgentModal } from './actions/upgrade-agent-modal';
+import { getOutdatedAgents } from '../services';
+import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types';
+import { getErrorOrchestrator } from '../../../react-services/common-services';
const searchBarWQLOptions = {
implicitQuery: {
@@ -47,6 +52,11 @@ const searchBarWQLOptions = {
},
};
+type AgentList = {
+ items: Agent[];
+ totalItems: number;
+};
+
const mapDispatchToProps = dispatch => ({
updateCurrentAgentData: data => dispatch(updateCurrentAgentData(data)),
});
@@ -70,16 +80,18 @@ export const AgentsTable = compose(
const [filters, setFilters] = useState(defaultFilters);
const [agent, setAgent] = useState();
const [reloadTable, setReloadTable] = useState(0);
- const [agentList, setAgentList] = useState<{
- items: Agent[];
- totalItems: number;
- }>({ items: [], totalItems: 0 });
+ const [agentList, setAgentList] = useState({
+ items: [],
+ totalItems: 0,
+ });
const [isEditGroupsVisible, setIsEditGroupsVisible] = useState(false);
+ const [isUpgradeModalVisible, setIsUpgradeModalVisible] = useState(false);
const [selectedItems, setSelectedItems] = useState([]);
const [allAgentsSelected, setAllAgentsSelected] = useState(false);
const [denyEditGroups] = useUserPermissionsRequirements([
{ action: 'group:modify_assignments', resource: 'group:id:*' },
]);
+ const [outdatedAgents, setOutdatedAgents] = useState([]);
useEffect(() => {
if (sessionStorage.getItem('wz-agents-overview-table-filter')) {
@@ -146,6 +158,31 @@ export const AgentsTable = compose(
setAllAgentsSelected(true);
};
+ const handleOnDataChange = async (data: AgentList) => {
+ setAgentList(data);
+
+ const agentIds = data?.items?.map(agent => agent.id);
+
+ try {
+ const outdatedAgents = await getOutdatedAgents(agentIds);
+ setOutdatedAgents(outdatedAgents);
+ } catch (error) {
+ setOutdatedAgents([]);
+ const options = {
+ context: `AgentsTable.getOutdatedAgents`,
+ level: UI_LOGGER_LEVELS.ERROR,
+ severity: UI_ERROR_SEVERITIES.BUSINESS,
+ store: true,
+ error: {
+ error,
+ message: error.message || error,
+ title: `Could not get outdated agents`,
+ },
+ };
+ getErrorOrchestrator().handleError(options);
+ }
+ };
+
const showSelectAllItems =
(selectedItems.length === agentList.items?.length &&
selectedItems.length < agentList.totalItems) ||
@@ -227,7 +264,9 @@ export const AgentsTable = compose(
!denyEditGroups,
setAgent,
setIsEditGroupsVisible,
+ setIsUpgradeModalVisible,
setFilters,
+ outdatedAgents,
)}
tableInitialSortingField='id'
tablePageSizeOptions={[10, 25, 50, 100]}
@@ -251,7 +290,7 @@ export const AgentsTable = compose(
}}
rowProps={getRowProps}
filters={filters}
- onDataChange={data => setAgentList(data)}
+ onDataChange={handleOnDataChange}
downloadCsv
showReload
showFieldSelector
@@ -404,7 +443,7 @@ export const AgentsTable = compose(
return (
{table}
- {isEditGroupsVisible ? (
+ {isEditGroupsVisible && agent ? (
reloadAgents()}
@@ -414,6 +453,16 @@ export const AgentsTable = compose(
}}
/>
) : null}
+ {isUpgradeModalVisible && agent ? (
+ reloadAgents()}
+ onClose={() => {
+ setIsEditGroupsVisible(false);
+ setAgent(undefined);
+ }}
+ />
+ ) : null}
);
});
diff --git a/plugins/main/public/components/endpoints-summary/table/columns.tsx b/plugins/main/public/components/endpoints-summary/table/columns.tsx
index 7e8e404c9c..cd309f52d5 100644
--- a/plugins/main/public/components/endpoints-summary/table/columns.tsx
+++ b/plugins/main/public/components/endpoints-summary/table/columns.tsx
@@ -4,7 +4,7 @@ import { AgentSynced } from '../../agents/agent-synced';
import { AgentStatus } from '../../agents/agent-status';
import { formatUIDate } from '../../../react-services/time-service';
import { GroupTruncate } from '../../common/util';
-import { EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiBadge } from '@elastic/eui';
import { Agent } from '../types';
// Columns with the property truncateText: true won't wrap the text
@@ -13,7 +13,9 @@ export const agentsTableColumns = (
allowEditGroups: boolean,
setAgent: (agents: Agent) => void,
setIsEditGroupsVisible: (visible: boolean) => void,
+ setIsUpgradeModalVisible: (visible: boolean) => void,
setFilters: (filters) => void,
+ outdatedAgents: Agent[],
) => [
{
field: 'id',
@@ -66,7 +68,18 @@ export const agentsTableColumns = (
sortable: true,
show: true,
searchable: true,
- width: '10%',
+ width: '100px',
+ render: (version, agent) => {
+ const isOutdated = !!outdatedAgents.find(
+ outdatedAgent => outdatedAgent.id === agent.id,
+ );
+ return (
+
+
{version}
+ {isOutdated ?
Outdated : null}
+
+ );
+ },
},
{
field: 'dateAdd',
@@ -128,6 +141,8 @@ export const agentsTableColumns = (
allowEditGroups,
setAgent,
setIsEditGroupsVisible,
+ setIsUpgradeModalVisible,
+ outdatedAgents,
),
},
];
diff --git a/plugins/main/public/components/endpoints-summary/types.ts b/plugins/main/public/components/endpoints-summary/types.ts
index f0d3dca0da..ce9a085954 100644
--- a/plugins/main/public/components/endpoints-summary/types.ts
+++ b/plugins/main/public/components/endpoints-summary/types.ts
@@ -30,3 +30,8 @@ export type Group = {
name: string;
count: number;
};
+
+export type ResponseUpgradeAgents = {
+ agent: string;
+ task_id: number;
+};
From 0f945b731e31c31ebae0364e0e6fa198c11c8a94 Mon Sep 17 00:00:00 2001
From: Luciano Gorza
Date: Tue, 5 Mar 2024 13:08:15 -0300
Subject: [PATCH 02/11] Add upgrade errors alert
---
plugins/main/common/constants.ts | 12 ++
.../endpoints-summary/hooks/index.ts | 1 +
.../endpoints-summary/hooks/upgrade-tasks.ts | 79 ++++++++
.../endpoints-summary/services/get-tasks.tsx | 60 +++++++
.../endpoints-summary/services/index.tsx | 1 +
.../table/actions/upgrade-agent-modal.tsx | 2 +-
.../endpoints-summary/table/agents-table.tsx | 4 +
.../table/upgrades-in-progress/table.tsx | 168 ++++++++++++++++++
.../upgrades-in-progress.tsx | 162 +++++++++++++++++
9 files changed, 488 insertions(+), 1 deletion(-)
create mode 100644 plugins/main/public/components/endpoints-summary/hooks/upgrade-tasks.ts
create mode 100644 plugins/main/public/components/endpoints-summary/services/get-tasks.tsx
create mode 100644 plugins/main/public/components/endpoints-summary/table/upgrades-in-progress/table.tsx
create mode 100644 plugins/main/public/components/endpoints-summary/table/upgrades-in-progress/upgrades-in-progress.tsx
diff --git a/plugins/main/common/constants.ts b/plugins/main/common/constants.ts
index 0d30df7489..4422dad170 100644
--- a/plugins/main/common/constants.ts
+++ b/plugins/main/common/constants.ts
@@ -376,6 +376,18 @@ export const AGENT_STATUS_CODE = [
},
];
+export const API_NAME_TASK_STATUS = {
+ DONE: 'Done',
+ IN_PROGRESS: 'In progress',
+ FAILED: 'Failed',
+} as const;
+
+export const UI_TASK_STATUS = [
+ API_NAME_TASK_STATUS.DONE,
+ API_NAME_TASK_STATUS.IN_PROGRESS,
+ API_NAME_TASK_STATUS.FAILED,
+];
+
// Documentation
export const DOCUMENTATION_WEB_BASE_URL = 'https://documentation.wazuh.com';
diff --git a/plugins/main/public/components/endpoints-summary/hooks/index.ts b/plugins/main/public/components/endpoints-summary/hooks/index.ts
index 063e5cc418..c0269a5890 100644
--- a/plugins/main/public/components/endpoints-summary/hooks/index.ts
+++ b/plugins/main/public/components/endpoints-summary/hooks/index.ts
@@ -1,2 +1,3 @@
export { useGetTotalAgents } from './agents';
export { useGetGroups } from './groups';
+export { useGetUpgradeTasks } from './upgrade-tasks';
diff --git a/plugins/main/public/components/endpoints-summary/hooks/upgrade-tasks.ts b/plugins/main/public/components/endpoints-summary/hooks/upgrade-tasks.ts
new file mode 100644
index 0000000000..3889bcb231
--- /dev/null
+++ b/plugins/main/public/components/endpoints-summary/hooks/upgrade-tasks.ts
@@ -0,0 +1,79 @@
+import { useState, useEffect } from 'react';
+import { getTasks } from '../services';
+import { API_NAME_TASK_STATUS } from '../../../../common/constants';
+
+export const useGetUpgradeTasks = reload => {
+ const [totalInProgressTasks, setTotalInProgressTasks] = useState();
+ const [getInProgressIsLoading, setGetInProgressIsLoading] = useState(true);
+ const [getInProgressError, setGetInProgressError] = useState();
+
+ const [totalErrorUpgradeTasks, setTotalErrorUpgradeTasks] =
+ useState();
+ const [getErrorIsLoading, setErrorIsLoading] = useState(true);
+ const [getErrorTasksError, setGetErrorTasksError] = useState();
+
+ const getUpgradesInProgress = async () => {
+ try {
+ setGetInProgressIsLoading(true);
+ const { total_affected_items } = await getTasks({
+ status: 'In progress',
+ command: 'upgrade',
+ limit: 1,
+ });
+ setTotalInProgressTasks(total_affected_items);
+ setGetInProgressError(undefined);
+ } catch (error: any) {
+ setGetInProgressError(error);
+ } finally {
+ setGetInProgressIsLoading(false);
+ }
+ };
+
+ const getUpgradesError = async () => {
+ try {
+ setErrorIsLoading(true);
+ const datetime = new Date();
+ datetime.setMinutes(datetime.getMinutes() - 60);
+ const formattedDate = datetime.toISOString();
+
+ const { total_affected_items } = await getTasks({
+ status: API_NAME_TASK_STATUS.FAILED,
+ command: 'upgrade',
+ limit: 1,
+ q: `last_update_time>${formattedDate}`,
+ });
+ setTotalErrorUpgradeTasks(total_affected_items);
+ setGetErrorTasksError(undefined);
+ } catch (error: any) {
+ setGetErrorTasksError(error);
+ } finally {
+ setErrorIsLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ const fetchData = async () => {
+ await getUpgradesInProgress();
+ await getUpgradesError();
+
+ if (totalInProgressTasks === 0) {
+ clearInterval(intervalId);
+ }
+ };
+
+ fetchData();
+
+ const intervalId = setInterval(fetchData, 3000);
+
+ return () => clearInterval(intervalId);
+ }, [totalInProgressTasks, reload]);
+
+ return {
+ getInProgressIsLoading,
+ totalInProgressTasks,
+ getInProgressError,
+ totalErrorUpgradeTasks: 5,
+ getErrorIsLoading,
+ getErrorTasksError,
+ };
+};
diff --git a/plugins/main/public/components/endpoints-summary/services/get-tasks.tsx b/plugins/main/public/components/endpoints-summary/services/get-tasks.tsx
new file mode 100644
index 0000000000..0e772173d4
--- /dev/null
+++ b/plugins/main/public/components/endpoints-summary/services/get-tasks.tsx
@@ -0,0 +1,60 @@
+import IApiResponse from '../../../react-services/interfaces/api-response.interface';
+import { WzRequest } from '../../../react-services/wz-request';
+import { Agent } from '../types';
+
+export const getTasks = async ({
+ status,
+ command,
+ offset,
+ limit,
+ q,
+ pageSize = 1000,
+}: {
+ status: string;
+ command: string;
+ offset?: number;
+ limit?: number;
+ q?: string;
+ pageSize?: number;
+}) => {
+ let queryOffset = offset ?? 0;
+ let queryLimit = limit && limit <= pageSize ? limit : pageSize;
+ let allAffectedItems: Agent[] = [];
+ let totalAffectedItems;
+
+ do {
+ const {
+ data: {
+ data: { affected_items, total_affected_items },
+ },
+ } = (await WzRequest.apiReq('GET', '/tasks/status', {
+ params: {
+ limit: queryLimit,
+ offset: queryOffset,
+ status,
+ command,
+ q,
+ wait_for_complete: true,
+ },
+ })) as IApiResponse;
+
+ if (totalAffectedItems === undefined) {
+ totalAffectedItems = total_affected_items;
+ }
+
+ allAffectedItems = allAffectedItems.concat(affected_items);
+
+ queryOffset += queryLimit;
+
+ const restItems = limit ? limit - allAffectedItems.length : pageSize;
+ queryLimit = restItems > pageSize ? pageSize : restItems;
+ } while (
+ queryOffset < totalAffectedItems &&
+ (!limit || allAffectedItems.length < limit)
+ );
+
+ return {
+ affected_items: allAffectedItems,
+ total_affected_items: totalAffectedItems,
+ };
+};
diff --git a/plugins/main/public/components/endpoints-summary/services/index.tsx b/plugins/main/public/components/endpoints-summary/services/index.tsx
index cd6d8c0401..17910f7e44 100644
--- a/plugins/main/public/components/endpoints-summary/services/index.tsx
+++ b/plugins/main/public/components/endpoints-summary/services/index.tsx
@@ -6,3 +6,4 @@ export { addAgentsToGroupService } from './add-agents-to-group';
export { getGroupsService } from './get-groups';
export { upgradeAgentsService } from './upgrade-agents';
export { getOutdatedAgents } from './get-outdated-agents';
+export { getTasks } from './get-tasks';
diff --git a/plugins/main/public/components/endpoints-summary/table/actions/upgrade-agent-modal.tsx b/plugins/main/public/components/endpoints-summary/table/actions/upgrade-agent-modal.tsx
index b7dd113300..0481854917 100644
--- a/plugins/main/public/components/endpoints-summary/table/actions/upgrade-agent-modal.tsx
+++ b/plugins/main/public/components/endpoints-summary/table/actions/upgrade-agent-modal.tsx
@@ -39,7 +39,7 @@ export const UpgradeAgentModal = compose(
try {
await upgradeAgentsService({ agentIds: [agent.id] });
- showToast('success', 'Upgrade agent', 'Agent successfully upgraded');
+ showToast('success', 'Upgrade agent', 'Upgrade task in progress');
reloadAgents();
} catch (error) {
const options = {
diff --git a/plugins/main/public/components/endpoints-summary/table/agents-table.tsx b/plugins/main/public/components/endpoints-summary/table/agents-table.tsx
index a782e22a4e..3eb02fd219 100644
--- a/plugins/main/public/components/endpoints-summary/table/agents-table.tsx
+++ b/plugins/main/public/components/endpoints-summary/table/agents-table.tsx
@@ -44,6 +44,7 @@ import { UpgradeAgentModal } from './actions/upgrade-agent-modal';
import { getOutdatedAgents } from '../services';
import { UI_ERROR_SEVERITIES } from '../../../react-services/error-orchestrator/types';
import { getErrorOrchestrator } from '../../../react-services/common-services';
+import { AgentUpgradesInProgress } from './upgrades-in-progress/upgrades-in-progress';
const searchBarWQLOptions = {
implicitQuery: {
@@ -230,6 +231,9 @@ export const AgentsTable = compose(
addOnTitle={selectedtemsRenderer}
actionButtons={({ filters }) => (
<>
+
+
+
{
+ const datetime = new Date();
+ datetime.setMinutes(datetime.getMinutes() - 60);
+ const formattedDate = datetime.toISOString();
+
+ const defaultFilters = {
+ q: `last_update_time>${formattedDate}`,
+ };
+
+ const searchBarWQLOptions = {
+ implicitQuery: {
+ query: 'id!=000',
+ conjunction: ';',
+ },
+ };
+
+ return (
+ formatUIDate(value),
+ },
+ {
+ field: 'last_update_time',
+ name: 'Last update',
+ sortable: true,
+ searchable: true,
+ show: true,
+ render: value => formatUIDate(value),
+ },
+ {
+ field: 'status',
+ name: 'Status',
+ width: '100px',
+ sortable: true,
+ searchable: true,
+ show: true,
+ render: value => (
+
+ {value}
+
+ ),
+ },
+ {
+ field: 'error_message',
+ name: 'Error',
+ show: true,
+ },
+ ]}
+ tableInitialSortingField='last_update_time'
+ tableInitialSortingDirection='desc'
+ tablePageSizeOptions={[10, 25, 50, 100]}
+ filters={{ defaultFilters }}
+ searchTable
+ searchBarWQL={{
+ suggestions: {
+ field(currentValue) {
+ return [
+ { label: 'agent_id', description: 'filter by agent id' },
+ { label: 'status', description: 'filter by status' },
+ {
+ label: 'create_time',
+ description: 'filter by creation date',
+ },
+ {
+ label: 'last_update_time',
+ description: 'filter by last update date',
+ },
+ { label: 'task_id', description: 'filter by task id' },
+ ];
+ },
+ value: async (currentValue, { field }) => {
+ try {
+ switch (field) {
+ case 'status':
+ return UI_TASK_STATUS.map(status => ({
+ label: status,
+ }));
+ case 'agent_id': {
+ const response = await WzRequest.apiReq('GET', '/agents', {
+ params: {
+ distinct: true,
+ limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT,
+ select: 'id',
+ sort: `+id`,
+ ...(currentValue
+ ? {
+ q: `${searchBarWQLOptions.implicitQuery.query}${searchBarWQLOptions.implicitQuery.conjunction}id~${currentValue}`,
+ }
+ : {
+ q: `${searchBarWQLOptions.implicitQuery.query}`,
+ }),
+ },
+ });
+ return response?.data?.data.affected_items.map(item => ({
+ label: getLodash(item, 'id'),
+ }));
+ }
+ }
+ } catch (error) {
+ return [];
+ }
+ },
+ },
+ validate: {
+ value: ({ formattedValue, value: rawValue }, { field }) => {
+ const value = formattedValue ?? rawValue;
+ if (value) {
+ if (['create_time', 'last_update_time'].includes(field)) {
+ const isCorrectDate =
+ /^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}:\d{2}(.\d{1,6})?Z?)?$/.test(
+ value,
+ );
+ return isCorrectDate
+ ? undefined
+ : `"${value}" is not a expected format. Valid formats: YYYY-MM-DD, YYYY-MM-DD HH:mm:ss, YYYY-MM-DDTHH:mm:ss, YYYY-MM-DDTHH:mm:ssZ.`;
+ }
+ }
+ },
+ },
+ }}
+ tableProps={{
+ tableLayout: 'auto',
+ }}
+ />
+ );
+};
diff --git a/plugins/main/public/components/endpoints-summary/table/upgrades-in-progress/upgrades-in-progress.tsx b/plugins/main/public/components/endpoints-summary/table/upgrades-in-progress/upgrades-in-progress.tsx
new file mode 100644
index 0000000000..26c2fb9bbe
--- /dev/null
+++ b/plugins/main/public/components/endpoints-summary/table/upgrades-in-progress/upgrades-in-progress.tsx
@@ -0,0 +1,162 @@
+import React, { useState, useEffect } from 'react';
+import {
+ EuiPanel,
+ EuiProgress,
+ EuiText,
+ EuiModal,
+ EuiModalHeader,
+ EuiModalHeaderTitle,
+ EuiModalBody,
+ EuiModalFooter,
+ EuiButton,
+ EuiButtonIcon,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiToolTip,
+ EuiIconTip,
+} from '@elastic/eui';
+import { useGetUpgradeTasks } from '../../hooks';
+import { UI_LOGGER_LEVELS } from '../../../../../common/constants';
+import { UI_ERROR_SEVERITIES } from '../../../../react-services/error-orchestrator/types';
+import { getErrorOrchestrator } from '../../../../react-services/common-services';
+import { AgentUpgradesTable } from './table';
+
+interface AgentUpgradesInProgress {
+ reload: any;
+}
+
+export const AgentUpgradesInProgress = ({
+ reload,
+}: AgentUpgradesInProgress) => {
+ const [isUpgrading, setIsUpgrading] = useState(false);
+ const {
+ totalInProgressTasks = 0,
+ getInProgressError,
+ totalErrorUpgradeTasks = 0,
+ getErrorTasksError,
+ } = useGetUpgradeTasks(reload);
+
+ const [isModalVisible, setIsModalVisible] = useState(false);
+
+ useEffect(() => {
+ if (totalInProgressTasks > 0) {
+ setIsUpgrading(true);
+ }
+ }, [totalInProgressTasks]);
+
+ if (getInProgressError) {
+ const options = {
+ context: `AgentUpgradesInProgress.useGetUpgradeTasks`,
+ level: UI_LOGGER_LEVELS.ERROR,
+ severity: UI_ERROR_SEVERITIES.BUSINESS,
+ store: true,
+ error: {
+ error: getInProgressError,
+ message: getInProgressError.message || getInProgressError,
+ title: `Could not get upgrade progress tasks`,
+ },
+ };
+ getErrorOrchestrator().handleError(options);
+ }
+
+ if (getErrorTasksError) {
+ const options = {
+ context: `AgentUpgradesInProgress.useGetUpgradeTasks`,
+ level: UI_LOGGER_LEVELS.ERROR,
+ severity: UI_ERROR_SEVERITIES.BUSINESS,
+ store: true,
+ error: {
+ error: getErrorTasksError,
+ message: getErrorTasksError.message || getErrorTasksError,
+ title: `Could not get upgrade error tasks`,
+ },
+ };
+ getErrorOrchestrator().handleError(options);
+ }
+
+ const handleOnCloseModal = () => setIsModalVisible(false);
+
+ return isUpgrading || totalErrorUpgradeTasks ? (
+ <>
+
+ {totalInProgressTasks > 0 ? (
+
+ ) : (
+
+ )}
+
+ {isUpgrading ? (
+
+
+ {totalInProgressTasks}
+ {`${
+ totalInProgressTasks === 1 ? ' Upgrade' : ' Upgrades'
+ } in progress`}
+
+
+ ) : null}
+ {isUpgrading && totalErrorUpgradeTasks ? (
+ /
+ ) : null}
+ {totalErrorUpgradeTasks ? (
+
+
+
+ {totalErrorUpgradeTasks}
+ {` Failed ${
+ totalErrorUpgradeTasks === 1 ? 'upgrade' : 'upgrades'
+ }`}
+
+
+
+
+
+
+ Upgrade task details
}>
+ setIsModalVisible(true)}
+ iconType='eye'
+ aria-label='Details'
+ />
+
+
+
+ ) : null}
+
+
+ {isModalVisible ? (
+
+
+
+ Upgrade agent tasks
+
+
+
+
+
+
+
+ Close
+
+
+
+ ) : null}
+ >
+ ) : null;
+};
From a0034ae639f5897a60f7c88c5c7e34293cc84d88 Mon Sep 17 00:00:00 2001
From: Luciano Gorza
Date: Tue, 5 Mar 2024 13:11:15 -0300
Subject: [PATCH 03/11] Update CHANGELOG
---
CHANGELOG.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6d61dfa8fe..fb5bf03621 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ All notable changes to the Wazuh app project will be documented in this file.
- Support for Wazuh 4.9.0
- Added AngularJS dependencies [#6145](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6145)
- Added edit groups action to Endpoints Summary [#6250](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6250)
+- Added upgrade agent action to Endpoints Summary [#6476](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6476)
- Added global actions add agents to groups and remove agents from groups to Endpoints Summary [#6274](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6274)
- Added propagation of updates from the table to dashboard visualizations in Endpoints summary [#6460](https://github.com/wazuh/wazuh-dashboard-plugins/pull/6460)
From 7a8d06d5c8c22f8d70019557535bdc760e066b29 Mon Sep 17 00:00:00 2001
From: Luciano Gorza
Date: Tue, 5 Mar 2024 16:49:08 -0300
Subject: [PATCH 04/11] Add unit tests
---
.../endpoints-summary/hooks/agents.test.ts | 2 +-
.../hooks/upgrade-tasks.test.ts | 78 +++++++++++++
.../endpoints-summary/hooks/upgrade-tasks.ts | 2 +-
...ent-to-group.tsx => add-agent-to-group.ts} | 0
...ts-to-group.tsx => add-agents-to-group.ts} | 0
...get-agents.test.tsx => get-agents.test.ts} | 0
...ndex.tsx => get-color-palette-by-index.ts} | 0
...get-groups.test.tsx => get-groups.test.ts} | 0
...ated-agents.tsx => get-outdated-agents.ts} | 0
.../services/get-tasks.test.ts | 78 +++++++++++++
.../services/{get-tasks.tsx => get-tasks.ts} | 0
.../__snapshots__/agents-table.test.tsx.snap | 15 ++-
.../upgrade-agent-modal.test.tsx.snap | 8 ++
.../actions/upgrade-agent-modal.test.tsx | 62 +++++++++++
.../upgrades-in-progress.test.tsx.snap | 103 ++++++++++++++++++
.../upgrades-in-progress.test.tsx | 57 ++++++++++
16 files changed, 400 insertions(+), 5 deletions(-)
create mode 100644 plugins/main/public/components/endpoints-summary/hooks/upgrade-tasks.test.ts
rename plugins/main/public/components/endpoints-summary/services/{add-agent-to-group.tsx => add-agent-to-group.ts} (100%)
rename plugins/main/public/components/endpoints-summary/services/{add-agents-to-group.tsx => add-agents-to-group.ts} (100%)
rename plugins/main/public/components/endpoints-summary/services/{get-agents.test.tsx => get-agents.test.ts} (100%)
rename plugins/main/public/components/endpoints-summary/services/{get-color-palette-by-index.tsx => get-color-palette-by-index.ts} (100%)
rename plugins/main/public/components/endpoints-summary/services/{get-groups.test.tsx => get-groups.test.ts} (100%)
rename plugins/main/public/components/endpoints-summary/services/{get-outdated-agents.tsx => get-outdated-agents.ts} (100%)
create mode 100644 plugins/main/public/components/endpoints-summary/services/get-tasks.test.ts
rename plugins/main/public/components/endpoints-summary/services/{get-tasks.tsx => get-tasks.ts} (100%)
create mode 100644 plugins/main/public/components/endpoints-summary/table/actions/__snapshots__/upgrade-agent-modal.test.tsx.snap
create mode 100644 plugins/main/public/components/endpoints-summary/table/actions/upgrade-agent-modal.test.tsx
create mode 100644 plugins/main/public/components/endpoints-summary/table/upgrades-in-progress/__snapshots__/upgrades-in-progress.test.tsx.snap
create mode 100644 plugins/main/public/components/endpoints-summary/table/upgrades-in-progress/upgrades-in-progress.test.tsx
diff --git a/plugins/main/public/components/endpoints-summary/hooks/agents.test.ts b/plugins/main/public/components/endpoints-summary/hooks/agents.test.ts
index 32afd959e7..1cc3725ac9 100644
--- a/plugins/main/public/components/endpoints-summary/hooks/agents.test.ts
+++ b/plugins/main/public/components/endpoints-summary/hooks/agents.test.ts
@@ -1,4 +1,4 @@
-import { renderHook, act } from '@testing-library/react-hooks';
+import { renderHook } from '@testing-library/react-hooks';
import { useGetTotalAgents } from './agents';
import { getAgentsService } from '../services';
diff --git a/plugins/main/public/components/endpoints-summary/hooks/upgrade-tasks.test.ts b/plugins/main/public/components/endpoints-summary/hooks/upgrade-tasks.test.ts
new file mode 100644
index 0000000000..ce4e8a8964
--- /dev/null
+++ b/plugins/main/public/components/endpoints-summary/hooks/upgrade-tasks.test.ts
@@ -0,0 +1,78 @@
+import { renderHook } from '@testing-library/react-hooks';
+import { getTasks } from '../services';
+import { useGetUpgradeTasks } from './upgrade-tasks';
+import { API_NAME_TASK_STATUS } from '../../../../common/constants';
+
+jest.mock('../services', () => ({
+ getTasks: jest.fn(),
+}));
+
+jest.useFakeTimers();
+jest.spyOn(global, 'clearInterval');
+
+describe('useGetUpgradeTasks hook', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should fetch initial data without any error', async () => {
+ const mockGetTasks = jest.requireMock('../services').getTasks;
+ mockGetTasks.mockImplementation(async ({ status }) => {
+ if (status === API_NAME_TASK_STATUS.IN_PROGRESS) {
+ return { total_affected_items: 5 };
+ }
+ return { total_affected_items: 2 };
+ });
+
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useGetUpgradeTasks(false),
+ );
+
+ expect(result.current.getInProgressIsLoading).toBe(true);
+ expect(result.current.totalInProgressTasks).toBeUndefined();
+ expect(result.current.getInProgressError).toBeUndefined();
+
+ expect(result.current.getErrorIsLoading).toBe(true);
+ expect(result.current.totalErrorUpgradeTasks).toBeUndefined();
+ expect(result.current.getErrorTasksError).toBeUndefined();
+
+ await waitForNextUpdate();
+ jest.advanceTimersByTime(500);
+
+ expect(result.current.getInProgressIsLoading).toBe(false);
+ expect(result.current.totalInProgressTasks).toBe(5);
+ expect(result.current.getInProgressError).toBeUndefined();
+
+ jest.advanceTimersByTime(500);
+
+ expect(result.current.getErrorIsLoading).toBe(false);
+ expect(result.current.totalErrorUpgradeTasks).toBe(2);
+ expect(result.current.getErrorTasksError).toBeUndefined();
+ });
+
+ it('should clear interval when totalInProgressTasks is 0', async () => {
+ const mockGetTasks = jest.requireMock('../services').getTasks;
+ mockGetTasks.mockResolvedValue({ total_affected_items: 0 });
+
+ const { waitForNextUpdate } = renderHook(() => useGetUpgradeTasks(false));
+
+ await waitForNextUpdate();
+ jest.advanceTimersByTime(500);
+
+ expect(clearInterval).toHaveBeenCalledTimes(2);
+ });
+
+ it('should handle error while fetching data', async () => {
+ const mockErrorMessage = 'Some error occurred';
+ (getTasks as jest.Mock).mockRejectedValue(mockErrorMessage);
+
+ const { result, waitForNextUpdate } = renderHook(() =>
+ useGetUpgradeTasks(0),
+ );
+
+ expect(result.current.getInProgressIsLoading).toBeTruthy();
+ await waitForNextUpdate();
+ expect(result.current.getInProgressError).toBe(mockErrorMessage);
+ expect(result.current.getInProgressIsLoading).toBeFalsy();
+ });
+});
diff --git a/plugins/main/public/components/endpoints-summary/hooks/upgrade-tasks.ts b/plugins/main/public/components/endpoints-summary/hooks/upgrade-tasks.ts
index 3889bcb231..6b6bb85d54 100644
--- a/plugins/main/public/components/endpoints-summary/hooks/upgrade-tasks.ts
+++ b/plugins/main/public/components/endpoints-summary/hooks/upgrade-tasks.ts
@@ -72,8 +72,8 @@ export const useGetUpgradeTasks = reload => {
getInProgressIsLoading,
totalInProgressTasks,
getInProgressError,
- totalErrorUpgradeTasks: 5,
getErrorIsLoading,
+ totalErrorUpgradeTasks,
getErrorTasksError,
};
};
diff --git a/plugins/main/public/components/endpoints-summary/services/add-agent-to-group.tsx b/plugins/main/public/components/endpoints-summary/services/add-agent-to-group.ts
similarity index 100%
rename from plugins/main/public/components/endpoints-summary/services/add-agent-to-group.tsx
rename to plugins/main/public/components/endpoints-summary/services/add-agent-to-group.ts
diff --git a/plugins/main/public/components/endpoints-summary/services/add-agents-to-group.tsx b/plugins/main/public/components/endpoints-summary/services/add-agents-to-group.ts
similarity index 100%
rename from plugins/main/public/components/endpoints-summary/services/add-agents-to-group.tsx
rename to plugins/main/public/components/endpoints-summary/services/add-agents-to-group.ts
diff --git a/plugins/main/public/components/endpoints-summary/services/get-agents.test.tsx b/plugins/main/public/components/endpoints-summary/services/get-agents.test.ts
similarity index 100%
rename from plugins/main/public/components/endpoints-summary/services/get-agents.test.tsx
rename to plugins/main/public/components/endpoints-summary/services/get-agents.test.ts
diff --git a/plugins/main/public/components/endpoints-summary/services/get-color-palette-by-index.tsx b/plugins/main/public/components/endpoints-summary/services/get-color-palette-by-index.ts
similarity index 100%
rename from plugins/main/public/components/endpoints-summary/services/get-color-palette-by-index.tsx
rename to plugins/main/public/components/endpoints-summary/services/get-color-palette-by-index.ts
diff --git a/plugins/main/public/components/endpoints-summary/services/get-groups.test.tsx b/plugins/main/public/components/endpoints-summary/services/get-groups.test.ts
similarity index 100%
rename from plugins/main/public/components/endpoints-summary/services/get-groups.test.tsx
rename to plugins/main/public/components/endpoints-summary/services/get-groups.test.ts
diff --git a/plugins/main/public/components/endpoints-summary/services/get-outdated-agents.tsx b/plugins/main/public/components/endpoints-summary/services/get-outdated-agents.ts
similarity index 100%
rename from plugins/main/public/components/endpoints-summary/services/get-outdated-agents.tsx
rename to plugins/main/public/components/endpoints-summary/services/get-outdated-agents.ts
diff --git a/plugins/main/public/components/endpoints-summary/services/get-tasks.test.ts b/plugins/main/public/components/endpoints-summary/services/get-tasks.test.ts
new file mode 100644
index 0000000000..eb06fbed5c
--- /dev/null
+++ b/plugins/main/public/components/endpoints-summary/services/get-tasks.test.ts
@@ -0,0 +1,78 @@
+import { API_NAME_TASK_STATUS } from '../../../../common/constants';
+import { WzRequest } from '../../../react-services/wz-request';
+import { getTasks } from './get-tasks';
+
+jest.mock('../../../react-services/wz-request', () => ({
+ WzRequest: {
+ apiReq: jest.fn(),
+ },
+}));
+
+describe('getTasks', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should retrieve tasks', async () => {
+ (WzRequest.apiReq as jest.Mock).mockResolvedValue({
+ data: {
+ data: {
+ affected_items: [
+ {
+ task_id: 1,
+ agent_id: '001',
+ status: API_NAME_TASK_STATUS.DONE,
+ command: 'upgrade',
+ },
+ {
+ task_id: 2,
+ agent_id: '002',
+ status: API_NAME_TASK_STATUS.DONE,
+ command: 'upgrade',
+ },
+ ],
+ total_affected_items: 2,
+ failed_items: [],
+ total_failed_items: 0,
+ },
+ error: 0,
+ message: 'Success',
+ },
+ });
+
+ const result = await getTasks({
+ status: API_NAME_TASK_STATUS.DONE,
+ command: 'upgrade',
+ limit: 10,
+ });
+
+ expect(WzRequest.apiReq).toHaveBeenCalledWith('GET', '/tasks/status', {
+ params: {
+ status: API_NAME_TASK_STATUS.DONE,
+ command: 'upgrade',
+ limit: 10,
+ offset: 0,
+ q: undefined,
+ wait_for_complete: true,
+ },
+ });
+
+ expect(result).toEqual({
+ affected_items: [
+ {
+ task_id: 1,
+ agent_id: '001',
+ status: API_NAME_TASK_STATUS.DONE,
+ command: 'upgrade',
+ },
+ {
+ task_id: 2,
+ agent_id: '002',
+ status: API_NAME_TASK_STATUS.DONE,
+ command: 'upgrade',
+ },
+ ],
+ total_affected_items: 2,
+ });
+ });
+});
diff --git a/plugins/main/public/components/endpoints-summary/services/get-tasks.tsx b/plugins/main/public/components/endpoints-summary/services/get-tasks.ts
similarity index 100%
rename from plugins/main/public/components/endpoints-summary/services/get-tasks.tsx
rename to plugins/main/public/components/endpoints-summary/services/get-tasks.ts
diff --git a/plugins/main/public/components/endpoints-summary/table/__snapshots__/agents-table.test.tsx.snap b/plugins/main/public/components/endpoints-summary/table/__snapshots__/agents-table.test.tsx.snap
index 5a0be00f9f..9978da4215 100644
--- a/plugins/main/public/components/endpoints-summary/table/__snapshots__/agents-table.test.tsx.snap
+++ b/plugins/main/public/components/endpoints-summary/table/__snapshots__/agents-table.test.tsx.snap
@@ -46,6 +46,9 @@ exports[`AgentsTable component Renders correctly to match the snapshot 1`] = `
+
@@ -562,7 +565,7 @@ exports[`AgentsTable component Renders correctly to match the snapshot 1`] = `
data-test-subj="tableHeaderCell_version_6"
role="columnheader"
scope="col"
- style="width:10%"
+ style="width:100px"
>