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" > + +
+
+
+ + +`; diff --git a/plugins/main/public/components/endpoints-summary/table/upgrades-in-progress/upgrades-in-progress.test.tsx b/plugins/main/public/components/endpoints-summary/table/upgrades-in-progress/upgrades-in-progress.test.tsx new file mode 100644 index 0000000000..0c5d62af0a --- /dev/null +++ b/plugins/main/public/components/endpoints-summary/table/upgrades-in-progress/upgrades-in-progress.test.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { render, fireEvent, waitFor, act } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { AgentUpgradesInProgress } from './upgrades-in-progress'; +import { useGetUpgradeTasks } from '../../hooks'; + +jest.mock('../../hooks', () => ({ + useGetUpgradeTasks: jest.fn(), +})); + +jest.mock('../../../../react-services/common-services', () => ({ + getErrorOrchestrator: () => ({ + handleError: () => {}, + }), +})); + +describe('AgentUpgradesInProgress component', () => { + test('should return the component', async () => { + (useGetUpgradeTasks as jest.Mock).mockReturnValue({ + getInProgressIsLoading: false, + totalInProgressTasks: 5, + getErrorIsLoading: false, + totalErrorUpgradeTasks: 2, + }); + + const { container, getByText } = render( + , + ); + + expect(container).toMatchSnapshot(); + + const inProgressValue = getByText('5'); + expect(inProgressValue).toBeInTheDocument(); + const inProgressText = getByText('Upgrades in progress'); + expect(inProgressText).toBeInTheDocument(); + + const failedValue = getByText('2'); + expect(failedValue).toBeInTheDocument(); + const failedText = getByText('Failed upgrades'); + expect(failedText).toBeInTheDocument(); + }); + + test('should show upgrade tasks modal', async () => { + const { getByRole, getByText } = render( + , + ); + + const openModalButton = getByRole('button'); + expect(openModalButton).toBeInTheDocument(); + + act(() => { + fireEvent.click(openModalButton); + }); + + await waitFor(() => expect(getByRole('table')).toBeInTheDocument()); + }); +}); From f91d41423c9f06e2c765a694f5bc88a341db6fa7 Mon Sep 17 00:00:00 2001 From: Luciano Gorza Date: Tue, 5 Mar 2024 18:10:39 -0300 Subject: [PATCH 05/11] Fix filter --- .../endpoints-summary/table/upgrades-in-progress/table.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/main/public/components/endpoints-summary/table/upgrades-in-progress/table.tsx b/plugins/main/public/components/endpoints-summary/table/upgrades-in-progress/table.tsx index ea0ad181ab..f008da5ecc 100644 --- a/plugins/main/public/components/endpoints-summary/table/upgrades-in-progress/table.tsx +++ b/plugins/main/public/components/endpoints-summary/table/upgrades-in-progress/table.tsx @@ -91,7 +91,7 @@ export const AgentUpgradesTable = () => { tableInitialSortingField='last_update_time' tableInitialSortingDirection='desc' tablePageSizeOptions={[10, 25, 50, 100]} - filters={{ defaultFilters }} + filters={defaultFilters} searchTable searchBarWQL={{ suggestions: { From a4adbecb27a6b99142c671b8cb1580034fa18f0b Mon Sep 17 00:00:00 2001 From: Luciano Gorza Date: Wed, 6 Mar 2024 12:20:40 -0300 Subject: [PATCH 06/11] Fix table filter suggestions --- .../table/upgrades-in-progress/table.tsx | 82 ++++++++++++------- .../upgrades-in-progress.tsx | 7 +- 2 files changed, 52 insertions(+), 37 deletions(-) diff --git a/plugins/main/public/components/endpoints-summary/table/upgrades-in-progress/table.tsx b/plugins/main/public/components/endpoints-summary/table/upgrades-in-progress/table.tsx index f008da5ecc..7f2bc4d095 100644 --- a/plugins/main/public/components/endpoints-summary/table/upgrades-in-progress/table.tsx +++ b/plugins/main/public/components/endpoints-summary/table/upgrades-in-progress/table.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { EuiHealth } from '@elastic/eui'; +import { EuiHealth, EuiIconTip } from '@elastic/eui'; import { TableWzAPI } from '../../../common/tables'; import { formatUIDate } from '../../../../react-services/time-service'; import { @@ -8,7 +8,7 @@ import { UI_TASK_STATUS, } from '../../../../../common/constants'; import { WzRequest } from '../../../../react-services/wz-request'; -import { get as getLodash } from 'lodash'; +import { get as getLodash, uniqBy as uniqByLodash } from 'lodash'; export const AgentUpgradesTable = () => { const datetime = new Date(); @@ -19,13 +19,6 @@ export const AgentUpgradesTable = () => { q: `last_update_time>${formattedDate}`, }; - const searchBarWQLOptions = { - implicitQuery: { - query: 'id!=000', - conjunction: ';', - }, - }; - return ( { }, { field: 'create_time', - name: 'Create', + name: ( + + Create{' '} + + + ), sortable: true, - searchable: true, + searchable: false, show: true, render: value => formatUIDate(value), }, { field: 'last_update_time', - name: 'Last update', + name: ( + + Last update{' '} + + + ), sortable: true, - searchable: true, + searchable: false, show: true, render: value => formatUIDate(value), }, @@ -86,12 +99,16 @@ export const AgentUpgradesTable = () => { field: 'error_message', name: 'Error', show: true, + searchable: true, }, ]} tableInitialSortingField='last_update_time' tableInitialSortingDirection='desc' tablePageSizeOptions={[10, 25, 50, 100]} filters={defaultFilters} + downloadCsv + showReload + showFieldSelector searchTable searchBarWQL={{ suggestions: { @@ -117,25 +134,28 @@ export const AgentUpgradesTable = () => { 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}`, - }), + default: { + const response = await WzRequest.apiReq( + 'GET', + '/tasks/status', + { + params: { + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue + ? { + q: `${field}~${currentValue}`, + } + : {}), + }, }, - }); - return response?.data?.data.affected_items.map(item => ({ - label: getLodash(item, 'id'), - })); + ); + const suggestionValues = + response?.data?.data.affected_items.map(item => ({ + label: getLodash(item, field), + })); + return uniqByLodash(suggestionValues, 'label'); } } } catch (error) { 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 index 26c2fb9bbe..5c0674e7d5 100644 --- 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 @@ -5,7 +5,6 @@ import { EuiText, EuiModal, EuiModalHeader, - EuiModalHeaderTitle, EuiModalBody, EuiModalFooter, EuiButton, @@ -142,11 +141,7 @@ export const AgentUpgradesInProgress = ({ {isModalVisible ? ( - - -

Upgrade agent tasks

-
-
+ From 1cb2f133bb0a0c1b18421d886e8b36fe8ce2ddd0 Mon Sep 17 00:00:00 2001 From: Luciano Gorza Date: Wed, 6 Mar 2024 12:59:14 -0300 Subject: [PATCH 07/11] Fix search bar table suggestions --- .../components/search-bar/query-language/wql.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/plugins/main/public/components/search-bar/query-language/wql.tsx b/plugins/main/public/components/search-bar/query-language/wql.tsx index 5282a6e096..7d139db27b 100644 --- a/plugins/main/public/components/search-bar/query-language/wql.tsx +++ b/plugins/main/public/components/search-bar/query-language/wql.tsx @@ -343,11 +343,13 @@ function filterTokenValueSuggestion( suggestions: QLOptionSuggestionEntityItemTyped[], ) { return suggestions - .filter(({ label }: QLOptionSuggestionEntityItemTyped) => { - const re = getTokenValueRegularExpression(); - return re.test(label); - }) - .slice(0, SEARCH_BAR_WQL_VALUE_SUGGESTIONS_DISPLAY_COUNT); + ? suggestions + .filter(({ label }: QLOptionSuggestionEntityItemTyped) => { + const re = getTokenValueRegularExpression(); + return re.test(label); + }) + .slice(0, SEARCH_BAR_WQL_VALUE_SUGGESTIONS_DISPLAY_COUNT) + : []; } /** From 48d708673b07a58f4fc67ae206d71dd145cccafa Mon Sep 17 00:00:00 2001 From: Luciano Gorza Date: Wed, 6 Mar 2024 22:08:50 -0300 Subject: [PATCH 08/11] Improve upgrade progress --- .../components/common/tables/table-wz-api.tsx | 13 +- .../endpoints-summary/hooks/upgrade-tasks.ts | 41 ++++- .../__snapshots__/agents-table.test.tsx.snap | 18 +-- .../endpoints-summary/table/agents-table.tsx | 4 +- .../endpoints-summary/table/columns.tsx | 27 +++- .../upgrades-in-progress.test.tsx.snap | 136 +++++++++------- .../upgrades-in-progress.test.tsx | 4 +- .../upgrades-in-progress.tsx | 146 +++++++++++------- 8 files changed, 247 insertions(+), 142 deletions(-) diff --git a/plugins/main/public/components/common/tables/table-wz-api.tsx b/plugins/main/public/components/common/tables/table-wz-api.tsx index 42821434de..ff2b6f966f 100644 --- a/plugins/main/public/components/common/tables/table-wz-api.tsx +++ b/plugins/main/public/components/common/tables/table-wz-api.tsx @@ -44,6 +44,9 @@ const getFilters = filters => { export function TableWzAPI({ actionButtons, + addOnTitle, + extra, + setReload, ...rest }: { actionButtons?: @@ -53,6 +56,7 @@ export function TableWzAPI({ title?: string; addOnTitle?: ReactNode; description?: string; + extra?: ReactNode; downloadCsv?: boolean | string; searchTable?: boolean; endpoint: string; @@ -166,8 +170,8 @@ export function TableWzAPI({ */ const triggerReload = () => { setReloadFootprint(Date.now()); - if (rest.setReload) { - rest.setReload(Date.now()); + if (setReload) { + setReload(Date.now()); } }; @@ -202,9 +206,9 @@ export function TableWzAPI({ )}
- {rest.addOnTitle ? ( + {addOnTitle ? ( - {rest.addOnTitle} + {addOnTitle} ) : null} @@ -297,6 +301,7 @@ export function TableWzAPI({ {rest.description} )} + {extra ? {extra} : null} {table} ); 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 6b6bb85d54..75e632c316 100644 --- a/plugins/main/public/components/endpoints-summary/hooks/upgrade-tasks.ts +++ b/plugins/main/public/components/endpoints-summary/hooks/upgrade-tasks.ts @@ -2,21 +2,32 @@ import { useState, useEffect } from 'react'; import { getTasks } from '../services'; import { API_NAME_TASK_STATUS } from '../../../../common/constants'; +const beforeMinutes = 60; + export const useGetUpgradeTasks = reload => { const [totalInProgressTasks, setTotalInProgressTasks] = useState(); const [getInProgressIsLoading, setGetInProgressIsLoading] = useState(true); const [getInProgressError, setGetInProgressError] = useState(); + const [totalSuccessTasks, setTotalSuccessTasks] = useState(); + const [getSuccessIsLoading, setSuccessIsLoading] = useState(true); + const [getSuccessError, setGetSuccessError] = useState(); + const [totalErrorUpgradeTasks, setTotalErrorUpgradeTasks] = useState(); const [getErrorIsLoading, setErrorIsLoading] = useState(true); const [getErrorTasksError, setGetErrorTasksError] = useState(); + const datetime = new Date(); + datetime.setMinutes(datetime.getMinutes() - beforeMinutes); + const formattedDate = datetime.toISOString(); + const timeFilter = `last_update_time>${formattedDate}`; + const getUpgradesInProgress = async () => { try { setGetInProgressIsLoading(true); const { total_affected_items } = await getTasks({ - status: 'In progress', + status: API_NAME_TASK_STATUS.IN_PROGRESS, command: 'upgrade', limit: 1, }); @@ -29,18 +40,32 @@ export const useGetUpgradeTasks = reload => { } }; + const getUpgradesSuccess = async () => { + try { + setSuccessIsLoading(true); + const { total_affected_items } = await getTasks({ + status: API_NAME_TASK_STATUS.DONE, + command: 'upgrade', + limit: 1, + q: timeFilter, + }); + setTotalSuccessTasks(total_affected_items); + setGetSuccessError(undefined); + } catch (error: any) { + setGetSuccessError(error); + } finally { + setSuccessIsLoading(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}`, + q: timeFilter, }); setTotalErrorUpgradeTasks(total_affected_items); setGetErrorTasksError(undefined); @@ -54,6 +79,7 @@ export const useGetUpgradeTasks = reload => { useEffect(() => { const fetchData = async () => { await getUpgradesInProgress(); + await getUpgradesSuccess(); await getUpgradesError(); if (totalInProgressTasks === 0) { @@ -72,6 +98,9 @@ export const useGetUpgradeTasks = reload => { getInProgressIsLoading, totalInProgressTasks, getInProgressError, + getSuccessIsLoading, + totalSuccessTasks, + getSuccessError, getErrorIsLoading, totalErrorUpgradeTasks, getErrorTasksError, 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 9978da4215..6cd96fd8bc 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,9 +46,6 @@ exports[`AgentsTable component Renders correctly to match the snapshot 1`] = `
-
@@ -227,6 +224,9 @@ exports[`AgentsTable component Renders correctly to match the snapshot 1`] = `
+
@@ -701,9 +701,6 @@ exports[`AgentsTable component Renders correctly to match the snapshot with cust
-
@@ -888,6 +885,9 @@ exports[`AgentsTable component Renders correctly to match the snapshot with cust
+
@@ -1315,9 +1315,6 @@ exports[`AgentsTable component Renders correctly to match the snapshot with no p
-
@@ -1502,6 +1499,9 @@ exports[`AgentsTable component Renders correctly to match the snapshot with no p
+
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 3eb02fd219..5890c5e0bb 100644 --- a/plugins/main/public/components/endpoints-summary/table/agents-table.tsx +++ b/plugins/main/public/components/endpoints-summary/table/agents-table.tsx @@ -229,11 +229,9 @@ export const AgentsTable = compose( } actionButtons={({ filters }) => ( <> - - - outdatedAgent.id === agent.id, ); return ( -
-
{version}
- {isOutdated ? Outdated : null} -
+ + {version} + {isOutdated ? ( + + Outdated

}> + +
+
+ ) : null} +
); }, }, diff --git a/plugins/main/public/components/endpoints-summary/table/upgrades-in-progress/__snapshots__/upgrades-in-progress.test.tsx.snap b/plugins/main/public/components/endpoints-summary/table/upgrades-in-progress/__snapshots__/upgrades-in-progress.test.tsx.snap index 3e287c05f7..a91fdfedfc 100644 --- a/plugins/main/public/components/endpoints-summary/table/upgrades-in-progress/__snapshots__/upgrades-in-progress.test.tsx.snap +++ b/plugins/main/public/components/endpoints-summary/table/upgrades-in-progress/__snapshots__/upgrades-in-progress.test.tsx.snap @@ -3,12 +3,8 @@ exports[`AgentUpgradesInProgress component should return the component 1`] = `
-
@@ -16,48 +12,27 @@ exports[`AgentUpgradesInProgress component should return the component 1`] = ` class="euiFlexItem euiFlexItem--flexGrowZero" >
- - 5 - - Upgrades in progress + Upgrade tasks
- / -
-
-
-
- - 2 - - Failed upgrades -
-
-
+ + Task details + -
+ +
+
+
+
+
- +
-
+
+
+
+
+ + +
- - - - + + +
diff --git a/plugins/main/public/components/endpoints-summary/table/upgrades-in-progress/upgrades-in-progress.test.tsx b/plugins/main/public/components/endpoints-summary/table/upgrades-in-progress/upgrades-in-progress.test.tsx index 0c5d62af0a..df4b3d67eb 100644 --- a/plugins/main/public/components/endpoints-summary/table/upgrades-in-progress/upgrades-in-progress.test.tsx +++ b/plugins/main/public/components/endpoints-summary/table/upgrades-in-progress/upgrades-in-progress.test.tsx @@ -31,12 +31,12 @@ describe('AgentUpgradesInProgress component', () => { const inProgressValue = getByText('5'); expect(inProgressValue).toBeInTheDocument(); - const inProgressText = getByText('Upgrades in progress'); + const inProgressText = getByText('Upgrade tasks in progress'); expect(inProgressText).toBeInTheDocument(); const failedValue = getByText('2'); expect(failedValue).toBeInTheDocument(); - const failedText = getByText('Failed upgrades'); + const failedText = getByText('Failed upgrade tasks'); expect(failedText).toBeInTheDocument(); }); 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 index 5c0674e7d5..6200317ef6 100644 --- 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 @@ -8,11 +8,11 @@ import { EuiModalBody, EuiModalFooter, EuiButton, - EuiButtonIcon, EuiFlexGroup, EuiFlexItem, - EuiToolTip, EuiIconTip, + EuiSpacer, + EuiButtonEmpty, } from '@elastic/eui'; import { useGetUpgradeTasks } from '../../hooks'; import { UI_LOGGER_LEVELS } from '../../../../../common/constants'; @@ -31,6 +31,8 @@ export const AgentUpgradesInProgress = ({ const { totalInProgressTasks = 0, getInProgressError, + totalSuccessTasks, + getSuccessError, totalErrorUpgradeTasks = 0, getErrorTasksError, } = useGetUpgradeTasks(reload); @@ -58,6 +60,21 @@ export const AgentUpgradesInProgress = ({ getErrorOrchestrator().handleError(options); } + if (getSuccessError) { + const options = { + context: `AgentUpgradesInProgress.useGetUpgradeTasks`, + level: UI_LOGGER_LEVELS.ERROR, + severity: UI_ERROR_SEVERITIES.BUSINESS, + store: true, + error: { + error: getSuccessError, + message: getSuccessError.message || getSuccessError, + title: `Could not get upgrade success tasks`, + }, + }; + getErrorOrchestrator().handleError(options); + } + if (getErrorTasksError) { const options = { context: `AgentUpgradesInProgress.useGetUpgradeTasks`, @@ -75,67 +92,82 @@ export const AgentUpgradesInProgress = ({ const handleOnCloseModal = () => setIsModalVisible(false); - return isUpgrading || totalErrorUpgradeTasks ? ( + return isUpgrading || totalSuccessTasks || totalErrorUpgradeTasks ? ( <> - - {totalInProgressTasks > 0 ? ( - - ) : ( - - )} + + + + Upgrade tasks + + + setIsModalVisible(true)} + iconType='eye' + > + Task details + + + + - {isUpgrading ? ( + {totalInProgressTasks > 0 ? ( - - {totalInProgressTasks} - {`${ - totalInProgressTasks === 1 ? ' Upgrade' : ' Upgrades' - } in progress`} - + + + + {totalInProgressTasks} + {totalInProgressTasks === 1 + ? ' Upgrade task in progress' + : ' Upgrade tasks in progress'} + + ) : null} - {isUpgrading && totalErrorUpgradeTasks ? ( - / + {totalSuccessTasks > 0 ? ( + + + + + + {totalSuccessTasks} + {totalSuccessTasks === 1 + ? ' Success upgrade task ' + : ' Success upgrade tasks '} + + + + + ) : null} - {totalErrorUpgradeTasks ? ( - - - - {totalErrorUpgradeTasks} - {` Failed ${ - totalErrorUpgradeTasks === 1 ? 'upgrade' : 'upgrades' - }`} - - - - - - - Upgrade task details

}> - setIsModalVisible(true)} - iconType='eye' - aria-label='Details' - /> -
-
-
+ {totalErrorUpgradeTasks > 0 ? ( + + + + + + {totalErrorUpgradeTasks} + {totalErrorUpgradeTasks === 1 + ? ' Failed upgrade task ' + : ' Failed upgrade tasks '} + + + + + + ) : null}
From 1f60ce4ba79063f91d9dac6961a42bab9243e77d Mon Sep 17 00:00:00 2001 From: Luciano Gorza Date: Fri, 8 Mar 2024 15:55:43 -0300 Subject: [PATCH 09/11] Improve API requests and modal refresh --- .../endpoints-summary/endpoints-summary.tsx | 1 + .../hooks/upgrade-tasks.test.ts | 19 +- .../endpoints-summary/hooks/upgrade-tasks.ts | 30 +-- .../endpoints-summary/table/agents-table.tsx | 3 +- .../table/upgrades-in-progress/table.tsx | 188 --------------- .../taskDetailsButton.tsx | 224 ++++++++++++++++++ .../upgrades-in-progress.tsx | 167 ++++++------- 7 files changed, 326 insertions(+), 306 deletions(-) delete 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/taskDetailsButton.tsx diff --git a/plugins/main/public/components/endpoints-summary/endpoints-summary.tsx b/plugins/main/public/components/endpoints-summary/endpoints-summary.tsx index 7817da0711..3ec1be73ca 100644 --- a/plugins/main/public/components/endpoints-summary/endpoints-summary.tsx +++ b/plugins/main/public/components/endpoints-summary/endpoints-summary.tsx @@ -149,6 +149,7 @@ export const EndpointsSummary = compose( 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 index ce4e8a8964..0978c9ac41 100644 --- a/plugins/main/public/components/endpoints-summary/hooks/upgrade-tasks.test.ts +++ b/plugins/main/public/components/endpoints-summary/hooks/upgrade-tasks.test.ts @@ -21,6 +21,9 @@ describe('useGetUpgradeTasks hook', () => { if (status === API_NAME_TASK_STATUS.IN_PROGRESS) { return { total_affected_items: 5 }; } + if (status === API_NAME_TASK_STATUS.DONE) { + return { total_affected_items: 3 }; + } return { total_affected_items: 2 }; }); @@ -29,11 +32,15 @@ describe('useGetUpgradeTasks hook', () => { ); expect(result.current.getInProgressIsLoading).toBe(true); - expect(result.current.totalInProgressTasks).toBeUndefined(); + expect(result.current.totalInProgressTasks).toBe(0); expect(result.current.getInProgressError).toBeUndefined(); + expect(result.current.getSuccessIsLoading).toBe(true); + expect(result.current.totalSuccessTasks).toBe(0); + expect(result.current.getSuccessError).toBeUndefined(); + expect(result.current.getErrorIsLoading).toBe(true); - expect(result.current.totalErrorUpgradeTasks).toBeUndefined(); + expect(result.current.totalErrorUpgradeTasks).toBe(0); expect(result.current.getErrorTasksError).toBeUndefined(); await waitForNextUpdate(); @@ -45,6 +52,12 @@ describe('useGetUpgradeTasks hook', () => { jest.advanceTimersByTime(500); + expect(result.current.getSuccessIsLoading).toBe(false); + expect(result.current.totalSuccessTasks).toBe(3); + expect(result.current.getSuccessError).toBeUndefined(); + + jest.advanceTimersByTime(500); + expect(result.current.getErrorIsLoading).toBe(false); expect(result.current.totalErrorUpgradeTasks).toBe(2); expect(result.current.getErrorTasksError).toBeUndefined(); @@ -59,7 +72,7 @@ describe('useGetUpgradeTasks hook', () => { await waitForNextUpdate(); jest.advanceTimersByTime(500); - expect(clearInterval).toHaveBeenCalledTimes(2); + expect(clearInterval).toHaveBeenCalledTimes(1); }); it('should handle error while fetching data', async () => { 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 75e632c316..f4ccd4a20a 100644 --- a/plugins/main/public/components/endpoints-summary/hooks/upgrade-tasks.ts +++ b/plugins/main/public/components/endpoints-summary/hooks/upgrade-tasks.ts @@ -4,17 +4,17 @@ import { API_NAME_TASK_STATUS } from '../../../../common/constants'; const beforeMinutes = 60; -export const useGetUpgradeTasks = reload => { - const [totalInProgressTasks, setTotalInProgressTasks] = useState(); +export const useGetUpgradeTasks = (reload: any) => { + const [totalInProgressTasks, setTotalInProgressTasks] = useState(0); const [getInProgressIsLoading, setGetInProgressIsLoading] = useState(true); const [getInProgressError, setGetInProgressError] = useState(); - const [totalSuccessTasks, setTotalSuccessTasks] = useState(); + const [totalSuccessTasks, setTotalSuccessTasks] = useState(0); const [getSuccessIsLoading, setSuccessIsLoading] = useState(true); const [getSuccessError, setGetSuccessError] = useState(); const [totalErrorUpgradeTasks, setTotalErrorUpgradeTasks] = - useState(); + useState(0); const [getErrorIsLoading, setErrorIsLoading] = useState(true); const [getErrorTasksError, setGetErrorTasksError] = useState(); @@ -76,20 +76,20 @@ export const useGetUpgradeTasks = reload => { } }; - useEffect(() => { - const fetchData = async () => { - await getUpgradesInProgress(); - await getUpgradesSuccess(); - await getUpgradesError(); - - if (totalInProgressTasks === 0) { - clearInterval(intervalId); - } - }; + const fetchData = async () => { + await getUpgradesInProgress(); + await getUpgradesSuccess(); + await getUpgradesError(); + }; + useEffect(() => { fetchData(); - const intervalId = setInterval(fetchData, 3000); + const intervalId = setInterval(getUpgradesInProgress, 3000); + + if (totalInProgressTasks === 0) { + clearInterval(intervalId); + } return () => clearInterval(intervalId); }, [totalInProgressTasks, reload]); 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 5890c5e0bb..640e24edd1 100644 --- a/plugins/main/public/components/endpoints-summary/table/agents-table.tsx +++ b/plugins/main/public/components/endpoints-summary/table/agents-table.tsx @@ -65,6 +65,7 @@ const mapDispatchToProps = dispatch => ({ interface AgentsTableProps { filters: any; updateCurrentAgentData: (agent) => void; + externalReload?: boolean; setExternalReload?: (newValue: number) => void; } @@ -229,7 +230,7 @@ export const AgentsTable = compose( } + extra={} actionButtons={({ filters }) => ( <> diff --git a/plugins/main/public/components/endpoints-summary/table/upgrades-in-progress/table.tsx b/plugins/main/public/components/endpoints-summary/table/upgrades-in-progress/table.tsx deleted file mode 100644 index 7f2bc4d095..0000000000 --- a/plugins/main/public/components/endpoints-summary/table/upgrades-in-progress/table.tsx +++ /dev/null @@ -1,188 +0,0 @@ -import React from 'react'; -import { EuiHealth, EuiIconTip } from '@elastic/eui'; -import { TableWzAPI } from '../../../common/tables'; -import { formatUIDate } from '../../../../react-services/time-service'; -import { - API_NAME_TASK_STATUS, - SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, - UI_TASK_STATUS, -} from '../../../../../common/constants'; -import { WzRequest } from '../../../../react-services/wz-request'; -import { get as getLodash, uniqBy as uniqByLodash } from 'lodash'; - -export const AgentUpgradesTable = () => { - const datetime = new Date(); - datetime.setMinutes(datetime.getMinutes() - 60); - const formattedDate = datetime.toISOString(); - - const defaultFilters = { - q: `last_update_time>${formattedDate}`, - }; - - return ( - - Create{' '} - - - ), - sortable: true, - searchable: false, - show: true, - render: value => formatUIDate(value), - }, - { - field: 'last_update_time', - name: ( - - Last update{' '} - - - ), - sortable: true, - searchable: false, - 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, - searchable: true, - }, - ]} - tableInitialSortingField='last_update_time' - tableInitialSortingDirection='desc' - tablePageSizeOptions={[10, 25, 50, 100]} - filters={defaultFilters} - downloadCsv - showReload - showFieldSelector - 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, - })); - default: { - const response = await WzRequest.apiReq( - 'GET', - '/tasks/status', - { - params: { - limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, - select: field, - sort: `+${field}`, - ...(currentValue - ? { - q: `${field}~${currentValue}`, - } - : {}), - }, - }, - ); - const suggestionValues = - response?.data?.data.affected_items.map(item => ({ - label: getLodash(item, field), - })); - return uniqByLodash(suggestionValues, 'label'); - } - } - } 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/taskDetailsButton.tsx b/plugins/main/public/components/endpoints-summary/table/upgrades-in-progress/taskDetailsButton.tsx new file mode 100644 index 0000000000..5e1753dde7 --- /dev/null +++ b/plugins/main/public/components/endpoints-summary/table/upgrades-in-progress/taskDetailsButton.tsx @@ -0,0 +1,224 @@ +import React, { useState, memo } from 'react'; +import { EuiHealth, EuiIconTip } from '@elastic/eui'; +import { TableWzAPI } from '../../../common/tables'; +import { formatUIDate } from '../../../../react-services/time-service'; +import { + API_NAME_TASK_STATUS, + SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + UI_TASK_STATUS, +} from '../../../../../common/constants'; +import { WzRequest } from '../../../../react-services/wz-request'; +import { get as getLodash, uniqBy as uniqByLodash } from 'lodash'; +import { + EuiModal, + EuiModalHeader, + EuiModalBody, + EuiModalFooter, + EuiButton, + EuiButtonEmpty, +} from '@elastic/eui'; + +export const AgentUpgradesTaskDetailsButton = memo(() => { + const [isModalVisible, setIsModalVisible] = useState(false); + + const datetime = new Date(); + datetime.setMinutes(datetime.getMinutes() - 60); + const formattedDate = datetime.toISOString(); + + const defaultFilters = { + q: `last_update_time>${formattedDate}`, + }; + + const handleOnCloseModal = () => setIsModalVisible(false); + + return ( + <> + setIsModalVisible(true)} + iconType='eye' + > + Task details + + {isModalVisible ? ( + + + + + Create{' '} + + + ), + sortable: true, + searchable: false, + show: true, + render: value => formatUIDate(value), + }, + { + field: 'last_update_time', + name: ( + + Last update{' '} + + + ), + sortable: true, + searchable: false, + 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, + searchable: true, + }, + ]} + tableInitialSortingField='last_update_time' + tableInitialSortingDirection='desc' + tablePageSizeOptions={[10, 25, 50, 100]} + filters={defaultFilters} + downloadCsv + showReload + showFieldSelector + 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, + })); + default: { + const response = await WzRequest.apiReq( + 'GET', + '/tasks/status', + { + params: { + limit: SEARCH_BAR_WQL_VALUE_SUGGESTIONS_COUNT, + select: field, + sort: `+${field}`, + ...(currentValue + ? { + q: `${field}~${currentValue}`, + } + : {}), + }, + }, + ); + const suggestionValues = + response?.data?.data.affected_items.map(item => ({ + label: getLodash(item, field), + })); + return uniqByLodash(suggestionValues, 'label'); + } + } + } 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', + }} + /> + + + + Close + + + + ) : null} + + ); +}); 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 index 6200317ef6..81b1133c94 100644 --- 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 @@ -3,22 +3,16 @@ import { EuiPanel, EuiProgress, EuiText, - EuiModal, - EuiModalHeader, - EuiModalBody, - EuiModalFooter, - EuiButton, EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiSpacer, - EuiButtonEmpty, } 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'; +import { AgentUpgradesTaskDetailsButton } from './taskDetailsButton'; interface AgentUpgradesInProgress { reload: any; @@ -29,16 +23,14 @@ export const AgentUpgradesInProgress = ({ }: AgentUpgradesInProgress) => { const [isUpgrading, setIsUpgrading] = useState(false); const { - totalInProgressTasks = 0, + totalInProgressTasks, getInProgressError, totalSuccessTasks, getSuccessError, - totalErrorUpgradeTasks = 0, + totalErrorUpgradeTasks, getErrorTasksError, } = useGetUpgradeTasks(reload); - const [isModalVisible, setIsModalVisible] = useState(false); - useEffect(() => { if (totalInProgressTasks > 0) { setIsUpgrading(true); @@ -90,100 +82,77 @@ export const AgentUpgradesInProgress = ({ getErrorOrchestrator().handleError(options); } - const handleOnCloseModal = () => setIsModalVisible(false); - return isUpgrading || totalSuccessTasks || totalErrorUpgradeTasks ? ( - <> - - + + + + Upgrade tasks + + + + + + + + {totalInProgressTasks > 0 ? ( - Upgrade tasks + + + + {totalInProgressTasks} + {totalInProgressTasks === 1 + ? ' Upgrade task in progress' + : ' Upgrade tasks in progress'} + + + ) : null} + {totalSuccessTasks > 0 ? ( - setIsModalVisible(true)} - iconType='eye' - > - Task details - - - - - - {totalInProgressTasks > 0 ? ( - - - + + + - {totalInProgressTasks} - {totalInProgressTasks === 1 - ? ' Upgrade task in progress' - : ' Upgrade tasks in progress'} + {totalSuccessTasks} + {totalSuccessTasks === 1 + ? ' Success upgrade task ' + : ' Success upgrade tasks '} + - - - ) : null} - {totalSuccessTasks > 0 ? ( - - - - - - {totalSuccessTasks} - {totalSuccessTasks === 1 - ? ' Success upgrade task ' - : ' Success upgrade tasks '} - - - - - - ) : null} - {totalErrorUpgradeTasks > 0 ? ( - - - - - - {totalErrorUpgradeTasks} - {totalErrorUpgradeTasks === 1 - ? ' Failed upgrade task ' - : ' Failed upgrade tasks '} + + + + ) : null} + {totalErrorUpgradeTasks > 0 ? ( + + + + + + {totalErrorUpgradeTasks} + {totalErrorUpgradeTasks === 1 + ? ' Failed upgrade task ' + : ' Failed upgrade tasks '} - - - - - - ) : null} - - - {isModalVisible ? ( - - - - - - - - Close - - - - ) : null} - + + + + + + ) : null} + +
) : null; }; From 620f4c5099b924a3a34f4e3801637bfd71042054 Mon Sep 17 00:00:00 2001 From: Luciano Gorza Date: Mon, 11 Mar 2024 12:36:25 -0300 Subject: [PATCH 10/11] Improve imposter response for task status --- docker/imposter/tasks/empty.json | 10 ++++ docker/imposter/tasks/status.js | 47 +++++++++++++++++++ docker/imposter/tasks/status_done.json | 22 +++++++++ docker/imposter/tasks/status_failed.json | 22 +++++++++ .../imposter/tasks/status_in_progress_1.json | 22 +++++++++ .../imposter/tasks/status_in_progress_2.json | 33 +++++++++++++ 6 files changed, 156 insertions(+) create mode 100644 docker/imposter/tasks/empty.json create mode 100644 docker/imposter/tasks/status.js create mode 100644 docker/imposter/tasks/status_done.json create mode 100644 docker/imposter/tasks/status_failed.json create mode 100644 docker/imposter/tasks/status_in_progress_1.json create mode 100644 docker/imposter/tasks/status_in_progress_2.json diff --git a/docker/imposter/tasks/empty.json b/docker/imposter/tasks/empty.json new file mode 100644 index 0000000000..086c883d12 --- /dev/null +++ b/docker/imposter/tasks/empty.json @@ -0,0 +1,10 @@ +{ + "data": { + "affected_items": [], + "total_affected_items": 0, + "total_failed_items": 0, + "failed_items": [] + }, + "message": "All specified task's status were returned", + "error": 0 +} diff --git a/docker/imposter/tasks/status.js b/docker/imposter/tasks/status.js new file mode 100644 index 0000000000..1e588f285e --- /dev/null +++ b/docker/imposter/tasks/status.js @@ -0,0 +1,47 @@ +var storeWazuh = stores.open('storeWazuh'); +var attemptRestart = storeWazuh.load('attempt'); + +var taskStatus = context.request.queryParams.status; + +if (!taskStatus) { + respond().withStatusCode(200).withFile('tasks/status_in_progress_2.json'); +} + +if (attemptRestart < 5) { + storeWazuh.save('attempt', attemptRestart + 1); + + if (taskStatus === 'In progress') { + respond().withStatusCode(200).withFile('tasks/status_in_progress_2.json'); + } + + if (taskStatus === 'Done' || taskStatus === 'Failed') { + respond().withStatusCode(200).withFile('tasks/empty.json'); + } +} else if (attemptRestart < 10) { + storeWazuh.save('attempt', attemptRestart + 1); + + if (taskStatus === 'In progress') { + respond().withStatusCode(200).withFile('tasks/status_in_progress_1.json'); + } + + if (taskStatus === 'Done') { + respond().withStatusCode(200).withFile('tasks/status_done.json'); + } + + if (taskStatus === 'Failed') { + respond().withStatusCode(200).withFile('tasks/empty.json'); + } +} else { + if (taskStatus === 'In progress') { + respond().withStatusCode(200).withFile('tasks/empty.json'); + } + + if (taskStatus === 'Done') { + respond().withStatusCode(200).withFile('tasks/status_done.json'); + } + + if (taskStatus === 'Failed') { + storeWazuh.save('attempt', 0); + respond().withStatusCode(200).withFile('tasks/status_failed.json'); + } +} diff --git a/docker/imposter/tasks/status_done.json b/docker/imposter/tasks/status_done.json new file mode 100644 index 0000000000..129a9c7948 --- /dev/null +++ b/docker/imposter/tasks/status_done.json @@ -0,0 +1,22 @@ +{ + "data": { + "affected_items": [ + { + "message": "Success", + "agent": "001", + "task_id": 1, + "node": "worker2", + "module": "upgrade_module", + "command": "upgrade", + "status": "Done", + "create_time": "2024-03-11T11:55:33.000Z", + "last_update_time": "2020-03-11T12:05:10.000Z" + } + ], + "total_affected_items": 1, + "total_failed_items": 0, + "failed_items": [] + }, + "message": "All specified task's status were returned", + "error": 0 +} diff --git a/docker/imposter/tasks/status_failed.json b/docker/imposter/tasks/status_failed.json new file mode 100644 index 0000000000..f4fe48c9da --- /dev/null +++ b/docker/imposter/tasks/status_failed.json @@ -0,0 +1,22 @@ +{ + "data": { + "affected_items": [ + { + "message": "Success", + "agent": "002", + "task_id": 2, + "node": "worker2", + "module": "upgrade_module", + "command": "upgrade", + "status": "Failed", + "create_time": "2024-03-11T11:57:44.000Z", + "last_update_time": "2020-03-11T12:11:32.000Z" + } + ], + "total_affected_items": 1, + "total_failed_items": 0, + "failed_items": [] + }, + "message": "All specified task's status were returned", + "error": 0 +} diff --git a/docker/imposter/tasks/status_in_progress_1.json b/docker/imposter/tasks/status_in_progress_1.json new file mode 100644 index 0000000000..865614332d --- /dev/null +++ b/docker/imposter/tasks/status_in_progress_1.json @@ -0,0 +1,22 @@ +{ + "data": { + "affected_items": [ + { + "message": "Success", + "agent": "002", + "task_id": 2, + "node": "worker2", + "module": "upgrade_module", + "command": "upgrade", + "status": "In progress", + "create_time": "2024-03-11T11:57:44.000Z", + "last_update_time": "2020-03-11T11:57:46.000Z" + } + ], + "total_affected_items": 1, + "total_failed_items": 0, + "failed_items": [] + }, + "message": "All specified task's status were returned", + "error": 0 +} diff --git a/docker/imposter/tasks/status_in_progress_2.json b/docker/imposter/tasks/status_in_progress_2.json new file mode 100644 index 0000000000..5e70a840c1 --- /dev/null +++ b/docker/imposter/tasks/status_in_progress_2.json @@ -0,0 +1,33 @@ +{ + "data": { + "affected_items": [ + { + "message": "Success", + "agent": "001", + "task_id": 1, + "node": "worker2", + "module": "upgrade_module", + "command": "upgrade", + "status": "In progress", + "create_time": "2024-03-11T11:55:33.000Z", + "last_update_time": "2020-03-11T11:55:36.000Z" + }, + { + "message": "Success", + "agent": "002", + "task_id": 2, + "node": "worker2", + "module": "upgrade_module", + "command": "upgrade", + "status": "In progress", + "create_time": "2024-03-11T11:57:44.000Z", + "last_update_time": "2020-03-11T11:57:46.000Z" + } + ], + "total_affected_items": 2, + "total_failed_items": 0, + "failed_items": [] + }, + "message": "All specified task's status were returned", + "error": 0 +} From 362f8f89670e9d5af1c8b1c24da24df04d407109 Mon Sep 17 00:00:00 2001 From: Luciano Gorza Date: Mon, 11 Mar 2024 12:39:28 -0300 Subject: [PATCH 11/11] Improve imposter response for task status --- docker/imposter/wazuh-config.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker/imposter/wazuh-config.yml b/docker/imposter/wazuh-config.yml index dd53ec3723..67a56da9ce 100755 --- a/docker/imposter/wazuh-config.yml +++ b/docker/imposter/wazuh-config.yml @@ -889,6 +889,9 @@ resources: # List tasks - method: GET path: /tasks/status + response: + statusCode: 200 + scriptFile: tasks/status.js # ===================================================== # # VULNERABILITY