mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Fleet] Add view agents button to activity flyout (#152555)
Part of https://github.com/elastic/kibana/issues/141206 ## Summary ### Server side: - Create a new API that returns Agents by actions Ids: ``` POST kbn:/api/fleet/agents { actionIds: [ 'action1', 'action2' ] } ``` ### UI: Add "view agents" button to activity flyout; when clicking on it, the button will take the user to the agent list and display only the subset of agents affected by the action. <img width="1100" alt="Screenshot 2023-03-09 at 16 27 41" src="https://user-images.githubusercontent.com/16084106/224072551-bf7b6cf3-9f32-4a79-8e61-d7dc35f4db54.png"> Also addiing a "clear filters" on top of the header to be able to remove the applied filter after the button is selected I also did some refactoring of the `AgentListPage` component, mostly extracted some small components and the `kueryBuilder` function, that became its own function and it's also been tested. ### Checklist - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
143a28b269
commit
eb60253fd1
23 changed files with 742 additions and 117 deletions
|
@ -1637,6 +1637,54 @@
|
|||
"basicAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"summary": "List agents by action ids",
|
||||
"tags": [
|
||||
"Agents"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/agent_get_by_actions"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"$ref": "#/components/responses/error"
|
||||
}
|
||||
},
|
||||
"operationId": "get-agents-by-actions",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/kbn_xsrf"
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"actionIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"policy_id"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/agents/bulk_upgrade": {
|
||||
|
@ -6137,6 +6185,16 @@
|
|||
"perPage"
|
||||
]
|
||||
},
|
||||
"agent_get_by_actions": {
|
||||
"title": "Agents get by action ids",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"bulk_upgrade_agents": {
|
||||
"title": "Bulk upgrade agents",
|
||||
"type": "object",
|
||||
|
|
|
@ -1021,6 +1021,35 @@ paths:
|
|||
type: boolean
|
||||
security:
|
||||
- basicAuth: []
|
||||
post:
|
||||
summary: List agents by action ids
|
||||
tags:
|
||||
- Agents
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/agent_get_by_actions'
|
||||
'400':
|
||||
$ref: '#/components/responses/error'
|
||||
operationId: get-agents-by-actions
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/kbn_xsrf'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
actionIds:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
required:
|
||||
- policy_id
|
||||
/agents/bulk_upgrade:
|
||||
post:
|
||||
summary: Bulk upgrade agents
|
||||
|
@ -3890,6 +3919,13 @@ components:
|
|||
- total
|
||||
- page
|
||||
- perPage
|
||||
agent_get_by_actions:
|
||||
title: Agents get by action ids
|
||||
type: array
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
bulk_upgrade_agents:
|
||||
title: Bulk upgrade agents
|
||||
type: object
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
title: Agents get by action ids
|
||||
type: array
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
|
@ -28,3 +28,32 @@ get:
|
|||
type: boolean
|
||||
security:
|
||||
- basicAuth: []
|
||||
post:
|
||||
summary: List agents by action ids
|
||||
tags:
|
||||
- Agents
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: ../components/schemas/agent_get_by_actions.yaml
|
||||
'400':
|
||||
$ref: ../components/responses/error.yaml
|
||||
operationId: get-agents-by-actions
|
||||
parameters:
|
||||
- $ref: ../components/headers/kbn_xsrf.yaml
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
actionIds:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
required:
|
||||
- policy_id
|
||||
|
|
|
@ -220,6 +220,7 @@ export const agentRouteService = {
|
|||
'{fileName}',
|
||||
fileName
|
||||
),
|
||||
getAgentsByActionsPath: () => AGENT_API_ROUTES.LIST_PATTERN,
|
||||
};
|
||||
|
||||
export const outputRoutesService = {
|
||||
|
|
|
@ -233,3 +233,13 @@ export interface GetActionStatusResponse {
|
|||
export interface GetAvailableVersionsResponse {
|
||||
items: string[];
|
||||
}
|
||||
|
||||
export interface PostRetrieveAgentsByActionsRequest {
|
||||
body: {
|
||||
actionIds: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface PostRetrieveAgentsByActionsResponse {
|
||||
items: string[];
|
||||
}
|
||||
|
|
|
@ -26,12 +26,16 @@ describe('AgentActivityFlyout', () => {
|
|||
const mockOnClose = jest.fn();
|
||||
const mockOnAbortSuccess = jest.fn();
|
||||
const mockAbortUpgrade = jest.fn();
|
||||
const mockSetSearch = jest.fn();
|
||||
const mockSetSelectedStatus = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
mockOnClose.mockReset();
|
||||
mockOnAbortSuccess.mockReset();
|
||||
mockAbortUpgrade.mockReset();
|
||||
mockUseActionStatus.mockReset();
|
||||
mockSetSearch.mockReset();
|
||||
mockSetSelectedStatus.mockReset();
|
||||
mockUseGetAgentPolicies.mockReturnValue({
|
||||
data: {
|
||||
items: [
|
||||
|
@ -60,6 +64,8 @@ describe('AgentActivityFlyout', () => {
|
|||
onClose={mockOnClose}
|
||||
onAbortSuccess={mockOnAbortSuccess}
|
||||
refreshAgentActivity={false}
|
||||
setSearch={mockSetSearch}
|
||||
setSelectedStatus={mockSetSelectedStatus}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import type { ReactNode } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { FormattedDate, FormattedMessage, FormattedTime } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
|
@ -24,16 +25,25 @@ import {
|
|||
EuiEmptyPrompt,
|
||||
EuiButtonEmpty,
|
||||
EuiFlyoutFooter,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import type { ActionStatus } from '../../../../types';
|
||||
import { useActionStatus } from '../hooks';
|
||||
import { useGetAgentPolicies, useStartServices } from '../../../../hooks';
|
||||
import {
|
||||
useGetAgentPolicies,
|
||||
useStartServices,
|
||||
sendPostRetrieveAgentsByActions,
|
||||
} from '../../../../hooks';
|
||||
import { SO_SEARCH_LIMIT } from '../../../../constants';
|
||||
|
||||
import { Loading } from '../../components';
|
||||
|
||||
import { getKuery } from '../utils/get_kuery';
|
||||
|
||||
import { AGENT_STATUSES } from '../../services/agent_status';
|
||||
|
||||
import { getTodayActions, getOtherDaysActions } from './agent_activity_helper';
|
||||
import { ViewErrors } from './view_errors';
|
||||
|
||||
|
@ -51,7 +61,10 @@ export const AgentActivityFlyout: React.FunctionComponent<{
|
|||
onClose: () => void;
|
||||
onAbortSuccess: () => void;
|
||||
refreshAgentActivity: boolean;
|
||||
}> = ({ onClose, onAbortSuccess, refreshAgentActivity }) => {
|
||||
setSearch: (search: string) => void;
|
||||
setSelectedStatus: (status: string[]) => void;
|
||||
}> = ({ onClose, onAbortSuccess, refreshAgentActivity, setSearch, setSelectedStatus }) => {
|
||||
const { notifications } = useStartServices();
|
||||
const { data: agentPoliciesData } = useGetAgentPolicies({
|
||||
perPage: SO_SEARCH_LIMIT,
|
||||
});
|
||||
|
@ -66,7 +79,7 @@ export const AgentActivityFlyout: React.FunctionComponent<{
|
|||
return policy?.name ?? policyId;
|
||||
};
|
||||
|
||||
const currentActionsEnriched = currentActions.map((a) => ({
|
||||
const currentActionsEnriched: ActionStatus[] = currentActions.map((a) => ({
|
||||
...a,
|
||||
newPolicyId: getAgentPolicyName(a.newPolicyId ?? ''),
|
||||
}));
|
||||
|
@ -78,6 +91,27 @@ export const AgentActivityFlyout: React.FunctionComponent<{
|
|||
const todayActions = getTodayActions(completedActions);
|
||||
const otherDays = getOtherDaysActions(completedActions);
|
||||
|
||||
const onClickViewAgents = async (action: ActionStatus) => {
|
||||
try {
|
||||
const { data } = await sendPostRetrieveAgentsByActions({ actionIds: [action.actionId] });
|
||||
if (data?.items?.length) {
|
||||
const kuery = getKuery({
|
||||
selectedAgentIds: data.items,
|
||||
});
|
||||
setSearch(kuery);
|
||||
}
|
||||
setSelectedStatus(AGENT_STATUSES);
|
||||
|
||||
onClose();
|
||||
} catch (err) {
|
||||
notifications.toasts.addError(err, {
|
||||
title: i18n.translate('xpack.fleet.agentActivityFlyout.error', {
|
||||
defaultMessage: 'Error viewing selected agents',
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlyout data-test-subj="agentActivityFlyout" onClose={onClose} size="m" paddingSize="none">
|
||||
|
@ -161,6 +195,7 @@ export const AgentActivityFlyout: React.FunctionComponent<{
|
|||
}
|
||||
actions={inProgressActions}
|
||||
abortUpgrade={abortUpgrade}
|
||||
onClickViewAgents={onClickViewAgents}
|
||||
/>
|
||||
) : null}
|
||||
{todayActions.length > 0 ? (
|
||||
|
@ -173,6 +208,7 @@ export const AgentActivityFlyout: React.FunctionComponent<{
|
|||
}
|
||||
actions={todayActions}
|
||||
abortUpgrade={abortUpgrade}
|
||||
onClickViewAgents={onClickViewAgents}
|
||||
/>
|
||||
) : null}
|
||||
{Object.keys(otherDays).map((day) => (
|
||||
|
@ -181,6 +217,7 @@ export const AgentActivityFlyout: React.FunctionComponent<{
|
|||
title={<FormattedDate value={day} year="numeric" month="short" day="2-digit" />}
|
||||
actions={otherDays[day]}
|
||||
abortUpgrade={abortUpgrade}
|
||||
onClickViewAgents={onClickViewAgents}
|
||||
/>
|
||||
))}
|
||||
</FullHeightFlyoutBody>
|
||||
|
@ -207,7 +244,8 @@ const ActivitySection: React.FunctionComponent<{
|
|||
title: ReactNode;
|
||||
actions: ActionStatus[];
|
||||
abortUpgrade: (action: ActionStatus) => Promise<void>;
|
||||
}> = ({ title, actions, abortUpgrade }) => {
|
||||
onClickViewAgents: (action: ActionStatus) => void;
|
||||
}> = ({ title, actions, abortUpgrade, onClickViewAgents }) => {
|
||||
return (
|
||||
<>
|
||||
<EuiPanel color="subdued" hasBorder={true} borderRadius="none">
|
||||
|
@ -221,9 +259,14 @@ const ActivitySection: React.FunctionComponent<{
|
|||
action={currentAction}
|
||||
abortUpgrade={abortUpgrade}
|
||||
key={currentAction.actionId}
|
||||
onClickViewAgents={onClickViewAgents}
|
||||
/>
|
||||
) : (
|
||||
<ActivityItem action={currentAction} key={currentAction.actionId} />
|
||||
<ActivityItem
|
||||
action={currentAction}
|
||||
key={currentAction.actionId}
|
||||
onClickViewAgents={onClickViewAgents}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
|
@ -323,7 +366,10 @@ const inProgressDescription = (time?: string) => (
|
|||
/>
|
||||
);
|
||||
|
||||
const ActivityItem: React.FunctionComponent<{ action: ActionStatus }> = ({ action }) => {
|
||||
const ActivityItem: React.FunctionComponent<{
|
||||
action: ActionStatus;
|
||||
onClickViewAgents: (action: ActionStatus) => void;
|
||||
}> = ({ action, onClickViewAgents }) => {
|
||||
const completeTitle = (
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
|
@ -509,6 +555,8 @@ const ActivityItem: React.FunctionComponent<{ action: ActionStatus }> = ({ actio
|
|||
) : null}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="xs" />
|
||||
<ViewAgentsButton action={action} onClickViewAgents={onClickViewAgents} />
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
@ -516,7 +564,8 @@ const ActivityItem: React.FunctionComponent<{ action: ActionStatus }> = ({ actio
|
|||
export const UpgradeInProgressActivityItem: React.FunctionComponent<{
|
||||
action: ActionStatus;
|
||||
abortUpgrade: (action: ActionStatus) => Promise<void>;
|
||||
}> = ({ action, abortUpgrade }) => {
|
||||
onClickViewAgents: (action: ActionStatus) => void;
|
||||
}> = ({ action, abortUpgrade, onClickViewAgents }) => {
|
||||
const { docLinks } = useStartServices();
|
||||
const [isAborting, setIsAborting] = useState(false);
|
||||
const onClickAbortUpgrade = useCallback(async () => {
|
||||
|
@ -601,6 +650,9 @@ export const UpgradeInProgressActivityItem: React.FunctionComponent<{
|
|||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ViewAgentsButton action={action} onClickViewAgents={onClickViewAgents} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{showCancelButton ? (
|
||||
<EuiButton
|
||||
|
@ -622,3 +674,22 @@ export const UpgradeInProgressActivityItem: React.FunctionComponent<{
|
|||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
||||
const ViewAgentsButton: React.FunctionComponent<{
|
||||
action: ActionStatus;
|
||||
onClickViewAgents: (action: ActionStatus) => void;
|
||||
}> = ({ action, onClickViewAgents }) => {
|
||||
return action.type !== 'UPDATE_TAGS' ? (
|
||||
<EuiButtonEmpty
|
||||
size="m"
|
||||
onClick={() => onClickViewAgents(action)}
|
||||
flush="left"
|
||||
data-test-subj="agentActivityFlyout.viewAgentsButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentActivityFlyout.viewAgentsButton"
|
||||
defaultMessage="View Agents"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
) : null;
|
||||
};
|
||||
|
|
|
@ -23,13 +23,15 @@ import { isAgentUpgradeable, ExperimentalFeaturesService } from '../../../../ser
|
|||
import { AgentHealth } from '../../components';
|
||||
|
||||
import type { Pagination } from '../../../../hooks';
|
||||
import { useLink, useKibanaVersion } from '../../../../hooks';
|
||||
import { useLink, useKibanaVersion, useAuthz } from '../../../../hooks';
|
||||
|
||||
import { AgentPolicySummaryLine } from '../../../../components';
|
||||
import { Tags } from '../../components/tags';
|
||||
import type { AgentMetrics } from '../../../../../../../common/types';
|
||||
import { formatAgentCPU, formatAgentMemory } from '../../services/agent_metrics';
|
||||
|
||||
import { EmptyPrompt } from './empty_prompt';
|
||||
|
||||
const VERSION_FIELD = 'local_metadata.elastic.agent.version';
|
||||
const HOSTNAME_FIELD = 'local_metadata.host.hostname';
|
||||
function safeMetadata(val: any) {
|
||||
|
@ -50,10 +52,18 @@ interface Props {
|
|||
tableRef?: React.Ref<any>;
|
||||
showUpgradeable: boolean;
|
||||
totalAgents?: number;
|
||||
noItemsMessage: JSX.Element;
|
||||
pagination: Pagination;
|
||||
onTableChange: (criteria: CriteriaWithPagination<Agent>) => void;
|
||||
pageSizeOptions: number[];
|
||||
isUsingFilter: boolean;
|
||||
setEnrollmentFlyoutState: (
|
||||
value: React.SetStateAction<{
|
||||
isOpen: boolean;
|
||||
selectedPolicyId?: string | undefined;
|
||||
}>
|
||||
) => void;
|
||||
clearFilters: () => void;
|
||||
isCurrentRequestIncremented: boolean;
|
||||
}
|
||||
|
||||
export const AgentListTable: React.FC<Props> = (props: Props) => {
|
||||
|
@ -65,15 +75,19 @@ export const AgentListTable: React.FC<Props> = (props: Props) => {
|
|||
sortField,
|
||||
sortOrder,
|
||||
tableRef,
|
||||
noItemsMessage,
|
||||
onTableChange,
|
||||
onSelectionChange,
|
||||
totalAgents = 0,
|
||||
showUpgradeable,
|
||||
pagination,
|
||||
pageSizeOptions,
|
||||
isUsingFilter,
|
||||
setEnrollmentFlyoutState,
|
||||
clearFilters,
|
||||
isCurrentRequestIncremented,
|
||||
} = props;
|
||||
|
||||
const hasFleetAllPrivileges = useAuthz().fleet.all;
|
||||
const { displayAgentMetrics } = ExperimentalFeaturesService.get();
|
||||
|
||||
const { getHref } = useLink();
|
||||
|
@ -88,6 +102,34 @@ export const AgentListTable: React.FC<Props> = (props: Props) => {
|
|||
return !isHosted;
|
||||
};
|
||||
|
||||
const noItemsMessage =
|
||||
isLoading && isCurrentRequestIncremented ? (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentList.loadingAgentsMessage"
|
||||
defaultMessage="Loading agents…"
|
||||
/>
|
||||
) : isUsingFilter ? (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentList.noFilteredAgentsPrompt"
|
||||
defaultMessage="No agents found. {clearFiltersLink}"
|
||||
values={{
|
||||
clearFiltersLink: (
|
||||
<EuiLink onClick={() => clearFilters()}>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentList.clearFiltersLinkText"
|
||||
defaultMessage="Clear filters"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<EmptyPrompt
|
||||
hasFleetAllPrivileges={hasFleetAllPrivileges}
|
||||
setEnrollmentFlyoutState={setEnrollmentFlyoutState}
|
||||
/>
|
||||
);
|
||||
|
||||
const sorting = {
|
||||
sort: {
|
||||
field: sortField,
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiLink } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import type { Agent, SimplifiedAgentStatus } from '../../../../types';
|
||||
|
||||
|
@ -25,6 +26,8 @@ export const AgentTableHeader: React.FunctionComponent<{
|
|||
setSelectionMode: (mode: SelectionMode) => void;
|
||||
selectedAgents: Agent[];
|
||||
setSelectedAgents: (agents: Agent[]) => void;
|
||||
clearFilters: () => void;
|
||||
isUsingFilter: boolean;
|
||||
}> = ({
|
||||
agentStatus,
|
||||
totalAgents,
|
||||
|
@ -34,20 +37,34 @@ export const AgentTableHeader: React.FunctionComponent<{
|
|||
selectedAgents,
|
||||
setSelectedAgents,
|
||||
showInactive,
|
||||
clearFilters,
|
||||
isUsingFilter,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<AgentsSelectionStatus
|
||||
totalAgents={totalAgents}
|
||||
selectableAgents={selectableAgents}
|
||||
selectionMode={selectionMode}
|
||||
setSelectionMode={setSelectionMode}
|
||||
selectedAgents={selectedAgents}
|
||||
setSelectedAgents={setSelectedAgents}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexGroup justifyContent="flexStart" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<AgentsSelectionStatus
|
||||
totalAgents={totalAgents}
|
||||
selectableAgents={selectableAgents}
|
||||
selectionMode={selectionMode}
|
||||
setSelectionMode={setSelectionMode}
|
||||
selectedAgents={selectedAgents}
|
||||
setSelectedAgents={setSelectedAgents}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{isUsingFilter ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLink onClick={() => clearFilters()}>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentList.header.clearFiltersLinkText"
|
||||
defaultMessage="Clear filters"
|
||||
/>
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
{agentStatus && (
|
||||
<AgentStatusBadges showInactive={showInactive} agentStatus={agentStatus} />
|
||||
|
|
|
@ -47,6 +47,10 @@ export function useActionStatus(onAbortSuccess: () => void, refreshAgentActivity
|
|||
if (refreshAgentActivity) {
|
||||
refreshActions();
|
||||
}
|
||||
return () => {
|
||||
setCurrentActions([]);
|
||||
setIsFirstLoading(true);
|
||||
};
|
||||
}, [refreshActions, refreshAgentActivity]);
|
||||
|
||||
const abortUpgrade = useCallback(
|
||||
|
|
|
@ -7,18 +7,14 @@
|
|||
import React, { useState, useMemo, useCallback, useRef, useEffect } from 'react';
|
||||
import { differenceBy, isEqual } from 'lodash';
|
||||
import type { EuiBasicTable } from '@elastic/eui';
|
||||
import { EuiLink } from '@elastic/eui';
|
||||
import { EuiSpacer, EuiPortal } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import { agentStatusesToSummary } from '../../../../../../common/services';
|
||||
|
||||
import type { Agent, AgentPolicy, SimplifiedAgentStatus } from '../../../types';
|
||||
import {
|
||||
usePagination,
|
||||
useAuthz,
|
||||
useGetAgentPolicies,
|
||||
sendGetAgents,
|
||||
sendGetAgentStatus,
|
||||
|
@ -35,7 +31,7 @@ import {
|
|||
ExperimentalFeaturesService,
|
||||
policyHasFleetServer,
|
||||
} from '../../../services';
|
||||
import { AGENTS_PREFIX, SO_SEARCH_LIMIT } from '../../../constants';
|
||||
import { SO_SEARCH_LIMIT } from '../../../constants';
|
||||
import {
|
||||
AgentReassignAgentPolicyModal,
|
||||
AgentUnenrollAgentModal,
|
||||
|
@ -54,7 +50,7 @@ import { TagsAddRemove } from './components/tags_add_remove';
|
|||
import { AgentActivityFlyout } from './components';
|
||||
import { TableRowActions } from './components/table_row_actions';
|
||||
import { AgentListTable } from './components/agent_list_table';
|
||||
import { EmptyPrompt } from './components/empty_prompt';
|
||||
import { getKuery } from './utils/get_kuery';
|
||||
|
||||
const REFRESH_INTERVAL_MS = 30000;
|
||||
|
||||
|
@ -64,7 +60,6 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
|
|||
const { notifications, cloud } = useStartServices();
|
||||
useBreadcrumbs('agent_list');
|
||||
const defaultKuery: string = (useUrlParams().urlParams.kuery as string) || '';
|
||||
const hasFleetAllPrivileges = useAuthz().fleet.all;
|
||||
|
||||
// Agent data states
|
||||
const [showUpgradeable, setShowUpgradeable] = useState<boolean>(false);
|
||||
|
@ -78,6 +73,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
|
|||
const { pagination, pageSizeOptions, setPagination } = usePagination();
|
||||
const [sortField, setSortField] = useState<keyof Agent>('enrolled_at');
|
||||
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
|
||||
|
||||
const VERSION_FIELD = 'local_metadata.elastic.agent.version';
|
||||
const HOSTNAME_FIELD = 'local_metadata.host.hostname';
|
||||
|
||||
|
@ -162,64 +158,19 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
|
|||
setSortOrder(sort!.direction);
|
||||
};
|
||||
|
||||
// Kuery
|
||||
const kuery = useMemo(() => {
|
||||
let kueryBuilder = search.trim();
|
||||
if (selectedAgentPolicies.length) {
|
||||
if (kueryBuilder) {
|
||||
kueryBuilder = `(${kueryBuilder}) and`;
|
||||
}
|
||||
kueryBuilder = `${kueryBuilder} ${AGENTS_PREFIX}.policy_id : (${selectedAgentPolicies
|
||||
.map((agentPolicy) => `"${agentPolicy}"`)
|
||||
.join(' or ')})`;
|
||||
}
|
||||
|
||||
if (selectedTags.length) {
|
||||
if (kueryBuilder) {
|
||||
kueryBuilder = `(${kueryBuilder}) and`;
|
||||
}
|
||||
kueryBuilder = `${kueryBuilder} ${AGENTS_PREFIX}.tags : (${selectedTags
|
||||
.map((tag) => `"${tag}"`)
|
||||
.join(' or ')})`;
|
||||
}
|
||||
|
||||
if (selectedStatus.length) {
|
||||
const kueryStatus = selectedStatus
|
||||
.map((status) => {
|
||||
switch (status) {
|
||||
case 'healthy':
|
||||
return AgentStatusKueryHelper.buildKueryForOnlineAgents();
|
||||
case 'unhealthy':
|
||||
return AgentStatusKueryHelper.buildKueryForErrorAgents();
|
||||
case 'offline':
|
||||
return AgentStatusKueryHelper.buildKueryForOfflineAgents();
|
||||
case 'updating':
|
||||
return AgentStatusKueryHelper.buildKueryForUpdatingAgents();
|
||||
case 'inactive':
|
||||
return AgentStatusKueryHelper.buildKueryForInactiveAgents();
|
||||
case 'unenrolled':
|
||||
return AgentStatusKueryHelper.buildKueryForUnenrolledAgents();
|
||||
}
|
||||
|
||||
return undefined;
|
||||
})
|
||||
.filter((statusKuery) => statusKuery !== undefined)
|
||||
.join(' or ');
|
||||
|
||||
if (kueryBuilder) {
|
||||
kueryBuilder = `(${kueryBuilder}) and (${kueryStatus})`;
|
||||
} else {
|
||||
kueryBuilder = kueryStatus;
|
||||
}
|
||||
}
|
||||
|
||||
return kueryBuilder;
|
||||
}, [search, selectedAgentPolicies, selectedTags, selectedStatus]);
|
||||
|
||||
const showInactive = useMemo(() => {
|
||||
return selectedStatus.some((status) => status === 'inactive' || status === 'unenrolled');
|
||||
}, [selectedStatus]);
|
||||
|
||||
const kuery = useMemo(() => {
|
||||
return getKuery({
|
||||
search,
|
||||
selectedAgentPolicies,
|
||||
selectedTags,
|
||||
selectedStatus,
|
||||
});
|
||||
}, [search, selectedAgentPolicies, selectedStatus, selectedTags]);
|
||||
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
const [agentsStatus, setAgentsStatus] = useState<
|
||||
{ [key in SimplifiedAgentStatus]: number } | undefined
|
||||
|
@ -275,6 +226,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
|
|||
currentRequestRef.current++;
|
||||
const currentRequest = currentRequestRef.current;
|
||||
isLoadingVar.current = true;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const [agentsResponse, totalInactiveAgentsResponse, agentTagsResponse] =
|
||||
|
@ -354,13 +306,13 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
|
|||
pagination.currentPage,
|
||||
pagination.pageSize,
|
||||
kuery,
|
||||
showInactive,
|
||||
showUpgradeable,
|
||||
allTags,
|
||||
notifications.toasts,
|
||||
sortField,
|
||||
sortOrder,
|
||||
showInactive,
|
||||
showUpgradeable,
|
||||
displayAgentMetrics,
|
||||
allTags,
|
||||
notifications.toasts,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -450,33 +402,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
|
|||
setShowAgentActivityTour({ isOpen: true });
|
||||
};
|
||||
|
||||
const noItemsMessage =
|
||||
isLoading && currentRequestRef?.current === 1 ? (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentList.loadingAgentsMessage"
|
||||
defaultMessage="Loading agents…"
|
||||
/>
|
||||
) : isUsingFilter ? (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentList.noFilteredAgentsPrompt"
|
||||
defaultMessage="No agents found. {clearFiltersLink}"
|
||||
values={{
|
||||
clearFiltersLink: (
|
||||
<EuiLink onClick={() => clearFilters()}>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.agentList.clearFiltersLinkText"
|
||||
defaultMessage="Clear filters"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<EmptyPrompt
|
||||
hasFleetAllPrivileges={hasFleetAllPrivileges}
|
||||
setEnrollmentFlyoutState={setEnrollmentFlyoutState}
|
||||
/>
|
||||
);
|
||||
const isCurrentRequestIncremented = currentRequestRef?.current === 1;
|
||||
return (
|
||||
<>
|
||||
{isAgentActivityFlyoutOpen ? (
|
||||
|
@ -485,6 +411,8 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
|
|||
onAbortSuccess={fetchData}
|
||||
onClose={() => setAgentActivityFlyoutOpen(false)}
|
||||
refreshAgentActivity={isLoading}
|
||||
setSearch={setSearch}
|
||||
setSelectedStatus={setSelectedStatus}
|
||||
/>
|
||||
</EuiPortal>
|
||||
) : null}
|
||||
|
@ -615,6 +543,8 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
|
|||
setSelectionMode('manual');
|
||||
}
|
||||
}}
|
||||
clearFilters={clearFilters}
|
||||
isUsingFilter={isUsingFilter}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
{/* Agent list table */}
|
||||
|
@ -631,8 +561,11 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
|
|||
showUpgradeable={showUpgradeable}
|
||||
onTableChange={onTableChange}
|
||||
pagination={pagination}
|
||||
noItemsMessage={noItemsMessage}
|
||||
totalAgents={Math.min(totalAgents, SO_SEARCH_LIMIT)}
|
||||
isUsingFilter={isUsingFilter}
|
||||
setEnrollmentFlyoutState={setEnrollmentFlyoutState}
|
||||
clearFilters={clearFilters}
|
||||
isCurrentRequestIncremented={isCurrentRequestIncremented}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getKuery } from './get_kuery';
|
||||
|
||||
describe('getKuery', () => {
|
||||
const search = 'base search';
|
||||
const selectedTags = ['tag_1', 'tag_2', 'tag_3'];
|
||||
const selectedAgentIds = ['agent_id1', 'agent_id2'];
|
||||
const selectedAgentPolicies = ['policy1', 'policy2', 'policy3'];
|
||||
const selectedStatus = ['healthy', 'unhealthy'];
|
||||
const healthyStatuses = ['healthy', 'updating'];
|
||||
const inactiveStatuses = ['unhealthy', 'offline', 'inactive', 'unenrolled'];
|
||||
|
||||
it('should return a kuery with base search', () => {
|
||||
expect(getKuery({ search })).toEqual('base search');
|
||||
});
|
||||
|
||||
it('should return a kuery with selected tags', () => {
|
||||
expect(getKuery({ selectedTags })).toEqual(
|
||||
'fleet-agents.tags : ("tag_1" or "tag_2" or "tag_3")'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return a kuery with selected agent policies', () => {
|
||||
expect(getKuery({ selectedAgentPolicies })).toEqual(
|
||||
'fleet-agents.policy_id : ("policy1" or "policy2" or "policy3")'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return a kuery with selected agent ids', () => {
|
||||
expect(getKuery({ selectedAgentIds })).toEqual(
|
||||
'fleet-agents.agent.id : ("agent_id1" or "agent_id2")'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return a kuery with healthy selected status', () => {
|
||||
expect(getKuery({ selectedStatus: healthyStatuses })).toEqual(
|
||||
'status:online or (status:updating or status:unenrolling or status:enrolling)'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return a kuery with unhealthy selected status', () => {
|
||||
expect(getKuery({ selectedStatus: inactiveStatuses })).toEqual(
|
||||
'(status:error or status:degraded) or status:offline or status:inactive or status:unenrolled'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return a kuery with a combination of previous kueries', () => {
|
||||
expect(getKuery({ search, selectedTags, selectedStatus })).toEqual(
|
||||
'((base search) and fleet-agents.tags : ("tag_1" or "tag_2" or "tag_3")) and (status:online or (status:error or status:degraded))'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return empty string if nothing is passed', () => {
|
||||
expect(getKuery({})).toEqual('');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AgentStatusKueryHelper } from '../../../../services';
|
||||
import { AGENTS_PREFIX } from '../../../../constants';
|
||||
|
||||
export const getKuery = ({
|
||||
search,
|
||||
selectedAgentPolicies,
|
||||
selectedTags,
|
||||
selectedStatus,
|
||||
selectedAgentIds,
|
||||
}: {
|
||||
search?: string;
|
||||
selectedAgentPolicies?: string[];
|
||||
selectedTags?: string[];
|
||||
selectedStatus?: string[];
|
||||
selectedAgentIds?: string[];
|
||||
}) => {
|
||||
let kueryBuilder = '';
|
||||
if (search) {
|
||||
kueryBuilder = search.trim();
|
||||
}
|
||||
|
||||
if (selectedAgentPolicies?.length) {
|
||||
if (kueryBuilder) {
|
||||
kueryBuilder = `(${kueryBuilder}) and`;
|
||||
}
|
||||
kueryBuilder = `${kueryBuilder} ${AGENTS_PREFIX}.policy_id : (${selectedAgentPolicies
|
||||
.map((agentPolicy) => `"${agentPolicy}"`)
|
||||
.join(' or ')})`;
|
||||
}
|
||||
|
||||
if (selectedTags?.length) {
|
||||
if (kueryBuilder) {
|
||||
kueryBuilder = `(${kueryBuilder}) and`;
|
||||
}
|
||||
kueryBuilder = `${kueryBuilder} ${AGENTS_PREFIX}.tags : (${selectedTags
|
||||
.map((tag) => `"${tag}"`)
|
||||
.join(' or ')})`;
|
||||
}
|
||||
|
||||
if (selectedAgentIds?.length) {
|
||||
if (kueryBuilder) {
|
||||
kueryBuilder = `(${kueryBuilder}) and`;
|
||||
}
|
||||
kueryBuilder = `${kueryBuilder} ${AGENTS_PREFIX}.agent.id : (${selectedAgentIds
|
||||
.map((id) => `"${id}"`)
|
||||
.join(' or ')})`;
|
||||
}
|
||||
|
||||
if (selectedStatus?.length) {
|
||||
const kueryStatus = selectedStatus
|
||||
.map((status) => {
|
||||
switch (status) {
|
||||
case 'healthy':
|
||||
return AgentStatusKueryHelper.buildKueryForOnlineAgents();
|
||||
case 'unhealthy':
|
||||
return AgentStatusKueryHelper.buildKueryForErrorAgents();
|
||||
case 'offline':
|
||||
return AgentStatusKueryHelper.buildKueryForOfflineAgents();
|
||||
case 'updating':
|
||||
return AgentStatusKueryHelper.buildKueryForUpdatingAgents();
|
||||
case 'inactive':
|
||||
return AgentStatusKueryHelper.buildKueryForInactiveAgents();
|
||||
case 'unenrolled':
|
||||
return AgentStatusKueryHelper.buildKueryForUnenrolledAgents();
|
||||
}
|
||||
|
||||
return undefined;
|
||||
})
|
||||
.filter((statusKuery) => statusKuery !== undefined)
|
||||
.join(' or ');
|
||||
|
||||
if (kueryBuilder) {
|
||||
kueryBuilder = `(${kueryBuilder}) and (${kueryStatus})`;
|
||||
} else {
|
||||
kueryBuilder = kueryStatus;
|
||||
}
|
||||
}
|
||||
return kueryBuilder.trim();
|
||||
};
|
|
@ -44,6 +44,8 @@ import type {
|
|||
PostNewAgentActionResponse,
|
||||
GetCurrentUpgradesResponse,
|
||||
GetAvailableVersionsResponse,
|
||||
PostRetrieveAgentsByActionsRequest,
|
||||
PostRetrieveAgentsByActionsResponse,
|
||||
} from '../../types';
|
||||
|
||||
import { useRequest, sendRequest } from './use_request';
|
||||
|
@ -264,6 +266,14 @@ export function sendPostCancelAction(actionId: string) {
|
|||
});
|
||||
}
|
||||
|
||||
export function sendPostRetrieveAgentsByActions(body: PostRetrieveAgentsByActionsRequest['body']) {
|
||||
return sendRequest<PostRetrieveAgentsByActionsResponse>({
|
||||
path: agentRouteService.getAgentsByActionsPath(),
|
||||
method: 'post',
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
export function sendPutAgentTagsUpdate(
|
||||
agentId: string,
|
||||
body: UpdateAgentRequest['body'],
|
||||
|
|
|
@ -133,6 +133,8 @@ export type {
|
|||
GetAvailableVersionsResponse,
|
||||
PostHealthCheckRequest,
|
||||
PostHealthCheckResponse,
|
||||
PostRetrieveAgentsByActionsRequest,
|
||||
PostRetrieveAgentsByActionsResponse,
|
||||
} from '../../common/types';
|
||||
export {
|
||||
entries,
|
||||
|
|
|
@ -30,6 +30,7 @@ import type {
|
|||
GetActionStatusResponse,
|
||||
GetAgentUploadsResponse,
|
||||
PostAgentReassignResponse,
|
||||
PostRetrieveAgentsByActionsResponse,
|
||||
} from '../../../common/types';
|
||||
import type {
|
||||
GetAgentsRequestSchema,
|
||||
|
@ -45,6 +46,7 @@ import type {
|
|||
PostBulkUpdateAgentTagsRequestSchema,
|
||||
GetActionStatusRequestSchema,
|
||||
GetAgentUploadFileRequestSchema,
|
||||
PostRetrieveAgentsByActionsRequestSchema,
|
||||
} from '../../types';
|
||||
import { defaultFleetErrorHandler } from '../../errors';
|
||||
import * as AgentService from '../../services/agents';
|
||||
|
@ -410,6 +412,23 @@ export const getActionStatusHandler: RequestHandler<
|
|||
}
|
||||
};
|
||||
|
||||
export const postRetrieveAgentsByActionsHandler: RequestHandler<
|
||||
undefined,
|
||||
undefined,
|
||||
TypeOf<typeof PostRetrieveAgentsByActionsRequestSchema.body>
|
||||
> = async (context, request, response) => {
|
||||
const coreContext = await context.core;
|
||||
const esClient = coreContext.elasticsearch.client.asInternalUser;
|
||||
|
||||
try {
|
||||
const agents = await AgentService.getAgentsByActionsIds(esClient, request.body.actionIds);
|
||||
const body: PostRetrieveAgentsByActionsResponse = { items: agents };
|
||||
return response.ok({ body });
|
||||
} catch (error) {
|
||||
return defaultFleetErrorHandler({ error, response });
|
||||
}
|
||||
};
|
||||
|
||||
export const getAgentUploadsHandler: RequestHandler<
|
||||
TypeOf<typeof GetOneAgentRequestSchema.params>
|
||||
> = async (context, request, response) => {
|
||||
|
|
|
@ -32,6 +32,7 @@ import {
|
|||
PostBulkRequestDiagnosticsActionRequestSchema,
|
||||
ListAgentUploadsRequestSchema,
|
||||
GetAgentUploadFileRequestSchema,
|
||||
PostRetrieveAgentsByActionsRequestSchema,
|
||||
} from '../../types';
|
||||
import * as AgentService from '../../services/agents';
|
||||
import type { FleetConfigType } from '../..';
|
||||
|
@ -56,6 +57,7 @@ import {
|
|||
getAgentUploadsHandler,
|
||||
getAgentUploadFileHandler,
|
||||
postAgentsReassignHandler,
|
||||
postRetrieveAgentsByActionsHandler,
|
||||
} from './handlers';
|
||||
import {
|
||||
postNewAgentActionHandlerBuilder,
|
||||
|
@ -168,6 +170,17 @@ export const registerAPIRoutes = (router: FleetAuthzRouter, config: FleetConfigT
|
|||
getAgentActions: AgentService.getAgentActions,
|
||||
})
|
||||
);
|
||||
// Get agents by Action_Ids
|
||||
router.post(
|
||||
{
|
||||
path: AGENT_API_ROUTES.LIST_PATTERN,
|
||||
validate: PostRetrieveAgentsByActionsRequestSchema,
|
||||
fleetAuthz: {
|
||||
fleet: { all: true }, // Authorizations?
|
||||
},
|
||||
},
|
||||
postRetrieveAgentsByActionsHandler
|
||||
);
|
||||
|
||||
router.post(
|
||||
{
|
||||
|
|
|
@ -10,7 +10,7 @@ import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
|||
import { createAppContextStartContractMock } from '../../mocks';
|
||||
import { appContextService } from '../app_context';
|
||||
|
||||
import { cancelAgentAction } from './actions';
|
||||
import { cancelAgentAction, getAgentsByActionsIds } from './actions';
|
||||
import { bulkUpdateAgents } from './crud';
|
||||
|
||||
jest.mock('./crud');
|
||||
|
@ -114,4 +114,59 @@ describe('Agent actions', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
describe('getAgentsByActionsIds', () => {
|
||||
const esClientMock = elasticsearchServiceMock.createElasticsearchClient();
|
||||
|
||||
it('should find agents by passing actions Ids', async () => {
|
||||
esClientMock.search.mockResolvedValue({
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_source: {
|
||||
action_id: 'action2',
|
||||
agents: ['agent3', 'agent4'],
|
||||
expiration: '2022-05-12T18:16:18.019Z',
|
||||
type: 'UPGRADE',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as any);
|
||||
const actionsIds = ['action2'];
|
||||
expect(await getAgentsByActionsIds(esClientMock, actionsIds)).toEqual(['agent3', 'agent4']);
|
||||
});
|
||||
|
||||
it('should find agents by passing multiple actions Ids', async () => {
|
||||
esClientMock.search.mockResolvedValue({
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_source: {
|
||||
action_id: 'action2',
|
||||
agents: ['agent3', 'agent4'],
|
||||
expiration: '2022-05-12T18:16:18.019Z',
|
||||
type: 'UPGRADE',
|
||||
},
|
||||
},
|
||||
{
|
||||
_source: {
|
||||
action_id: 'action3',
|
||||
agents: ['agent5', 'agent6', 'agent7'],
|
||||
expiration: '2022-05-12T18:16:18.019Z',
|
||||
type: 'UNENROLL',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as any);
|
||||
const actionsIds = ['action2', 'actions3'];
|
||||
expect(await getAgentsByActionsIds(esClientMock, actionsIds)).toEqual([
|
||||
'agent3',
|
||||
'agent4',
|
||||
'agent5',
|
||||
'agent6',
|
||||
'agent7',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -195,7 +195,7 @@ export async function getAgentActions(esClient: ElasticsearchClient, actionId: s
|
|||
return res.hits.hits.map((hit) => ({
|
||||
...hit._source,
|
||||
id: hit._id,
|
||||
}));
|
||||
})) as FleetServerAgentAction[];
|
||||
}
|
||||
|
||||
export async function getUnenrollAgentActions(
|
||||
|
@ -347,6 +347,41 @@ export async function cancelAgentAction(esClient: ElasticsearchClient, actionId:
|
|||
} as AgentAction;
|
||||
}
|
||||
|
||||
async function getAgentActionsByIds(esClient: ElasticsearchClient, actionIds: string[]) {
|
||||
const res = await esClient.search<FleetServerAgentAction>({
|
||||
index: AGENT_ACTIONS_INDEX,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
terms: {
|
||||
action_id: actionIds,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
size: SO_SEARCH_LIMIT,
|
||||
});
|
||||
|
||||
if (res.hits.hits.length === 0) {
|
||||
throw new AgentActionNotFoundError('Action not found');
|
||||
}
|
||||
|
||||
return res.hits.hits.map((hit) => ({
|
||||
...hit._source,
|
||||
id: hit._id,
|
||||
})) as FleetServerAgentAction[];
|
||||
}
|
||||
|
||||
export const getAgentsByActionsIds = async (
|
||||
esClient: ElasticsearchClient,
|
||||
actionsIds: string[]
|
||||
) => {
|
||||
const actions = await getAgentActionsByIds(esClient, actionsIds);
|
||||
return actions.flatMap((a) => a?.agents).filter((agent) => !!agent) as string[];
|
||||
};
|
||||
|
||||
export interface ActionsService {
|
||||
getAgent: (
|
||||
esClient: ElasticsearchClient,
|
||||
|
@ -360,6 +395,5 @@ export interface ActionsService {
|
|||
esClient: ElasticsearchClient,
|
||||
newAgentAction: Omit<AgentAction, 'id'>
|
||||
) => Promise<AgentAction>;
|
||||
|
||||
getAgentActions: (esClient: ElasticsearchClient, actionId: string) => Promise<any[]>;
|
||||
}
|
||||
|
|
|
@ -60,6 +60,12 @@ export const PostCancelActionRequestSchema = {
|
|||
}),
|
||||
};
|
||||
|
||||
export const PostRetrieveAgentsByActionsRequestSchema = {
|
||||
body: schema.object({
|
||||
actionIds: schema.arrayOf(schema.string()),
|
||||
}),
|
||||
};
|
||||
|
||||
export const PostAgentUnenrollRequestSchema = {
|
||||
params: schema.object({
|
||||
agentId: schema.string(),
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
|
||||
|
||||
export default function (providerContext: FtrProviderContext) {
|
||||
const { getService } = providerContext;
|
||||
const esArchiver = getService('esArchiver');
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('es');
|
||||
|
||||
describe('fleet_get_agents_by_actions', () => {
|
||||
before(async () => {
|
||||
await esArchiver.load('x-pack/test/functional/es_archives/fleet/agents');
|
||||
await es.create({
|
||||
index: '.fleet-actions',
|
||||
refresh: 'wait_for',
|
||||
id: uuidv4(),
|
||||
body: {
|
||||
'@timestamp': new Date().toISOString(),
|
||||
expiration: new Date().toISOString(),
|
||||
agents: ['agent1', 'agent2'],
|
||||
action_id: 'action000001',
|
||||
data: {},
|
||||
type: 'UPGRADE',
|
||||
},
|
||||
});
|
||||
|
||||
await es.create({
|
||||
index: '.fleet-actions',
|
||||
refresh: 'wait_for',
|
||||
id: uuidv4(),
|
||||
body: {
|
||||
'@timestamp': new Date().toISOString(),
|
||||
expiration: new Date().toISOString(),
|
||||
agents: ['agent3', 'agent4', 'agent5'],
|
||||
action_id: 'action000002',
|
||||
data: {},
|
||||
type: 'UNENROLL',
|
||||
},
|
||||
});
|
||||
|
||||
await es.create({
|
||||
index: '.fleet-actions',
|
||||
refresh: 'wait_for',
|
||||
id: uuidv4(),
|
||||
body: {
|
||||
'@timestamp': new Date().toISOString(),
|
||||
expiration: new Date().toISOString(),
|
||||
action_id: 'action000003',
|
||||
data: {},
|
||||
type: 'UNENROLL',
|
||||
},
|
||||
});
|
||||
});
|
||||
after(async () => {
|
||||
await esArchiver.unload('x-pack/test/functional/es_archives/fleet/agents');
|
||||
});
|
||||
|
||||
describe('POST /agents/', () => {
|
||||
it('should return a list of agents corresponding to the payload action_ids', async () => {
|
||||
const { body: apiResponse } = await supertest
|
||||
.post(`/api/fleet/agents`)
|
||||
.set('kbn-xsrf', 'xx')
|
||||
.send({
|
||||
actionIds: ['action000001', 'action000002'],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(apiResponse.items).to.eql(['agent3', 'agent4', 'agent5', 'agent1', 'agent2']);
|
||||
});
|
||||
|
||||
it('should return a list of agents corresponding to the payload action_ids', async () => {
|
||||
const { body: apiResponse } = await supertest
|
||||
.post(`/api/fleet/agents`)
|
||||
.set('kbn-xsrf', 'xx')
|
||||
.send({
|
||||
actionIds: ['action000002'],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(apiResponse.items).to.eql(['agent3', 'agent4', 'agent5']);
|
||||
});
|
||||
|
||||
it('should return an empty list of agents if there are not agents on the action', async () => {
|
||||
const { body: apiResponse } = await supertest
|
||||
.post(`/api/fleet/agents`)
|
||||
.set('kbn-xsrf', 'xx')
|
||||
.send({
|
||||
actionIds: ['action000003'],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(apiResponse.items).to.eql([]);
|
||||
});
|
||||
|
||||
it('should return a 404 when action_ids are empty', async () => {
|
||||
await supertest
|
||||
.post(`/api/fleet/agents`)
|
||||
.set('kbn-xsrf', 'xx')
|
||||
.send({
|
||||
actionIds: [],
|
||||
})
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it('should return a 404 when action_ids do not exist', async () => {
|
||||
await supertest
|
||||
.post(`/api/fleet/agents`)
|
||||
.set('kbn-xsrf', 'xx')
|
||||
.send({
|
||||
actionIds: ['non_existent_action'],
|
||||
})
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -20,5 +20,6 @@ export default function loadTests({ loadTestFile }) {
|
|||
loadTestFile(require.resolve('./available_versions'));
|
||||
loadTestFile(require.resolve('./request_diagnostics'));
|
||||
loadTestFile(require.resolve('./uploads'));
|
||||
loadTestFile(require.resolve('./get_agents_by_actions'));
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue