Skip to content

Commit

Permalink
[Ingest Manager] Agent bulk actions UI (elastic#77690)
Browse files Browse the repository at this point in the history
* Add temporary client-side license service/hook

* Initial pass at bulk actions UI (UI behavior only)

* Initial pass at implementing reassign agent policy by agent IDs

* Allow bulk reassign agent policy by kuery

* Return total inactive agents in list agents API to better handle bulk action selection UI that may or may not include active agents

* Add isGoldPlus method to license service

* Add `normalizeKuery` function

* Add `.findAllSOs` method and refactor bulk reassign to use that

* Initial pass at backend work for bulk unenroll

* Covert unenroll provider to unenroll modal and adjust UI to include force option

* Move license protection to handler level, fix misc bugs

* Add comments about `data` field response in create agent action(s)

* Clean up license service

* Fix i18n

* Add tests for bulk. unenroll

* Add tests for reassign and bulk reassign

* Fix typing

* Adjust single actions icon and text to be consistent

* Fix i18n

* PR feedback

* Increment api key test assertion to account for adding another agent policy to es archiver data

* Fix test

* Fix duplicate declaration after merging

* Add comments to SO find all function

* Batch invalidate API keys requests
# Conflicts:
#	x-pack/plugins/ingest_manager/server/services/index.ts
  • Loading branch information
jen-huang committed Sep 22, 2020
1 parent 52c37b8 commit 4357df7
Show file tree
Hide file tree
Showing 42 changed files with 1,416 additions and 437 deletions.
2 changes: 2 additions & 0 deletions x-pack/plugins/ingest_manager/common/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ export const AGENT_API_ROUTES = {
ACTIONS_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/actions`,
ENROLL_PATTERN: `${FLEET_API_ROOT}/agents/enroll`,
UNENROLL_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/unenroll`,
BULK_UNENROLL_PATTERN: `${FLEET_API_ROOT}/agents/bulk_unenroll`,
REASSIGN_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/reassign`,
BULK_REASSIGN_PATTERN: `${FLEET_API_ROOT}/agents/bulk_reassign`,
STATUS_PATTERN: `${FLEET_API_ROOT}/agent-status`,
};

Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/ingest_manager/common/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export { isPackageLimited, doesAgentPolicyAlreadyIncludePackage } from './limite
export { decodeCloudId } from './decode_cloud_id';
export { isValidNamespace } from './is_valid_namespace';
export { isDiffPathProtocol } from './is_diff_path_protocol';
export { LicenseService } from './license';
46 changes: 46 additions & 0 deletions x-pack/plugins/ingest_manager/common/services/license.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Observable, Subscription } from 'rxjs';
import { ILicense } from '../../../licensing/common/types';

// Generic license service class that works with the license observable
// Both server and client plugins instancates a singleton version of this class
export class LicenseService {
private observable: Observable<ILicense> | null = null;
private subscription: Subscription | null = null;
private licenseInformation: ILicense | null = null;

private updateInformation(licenseInformation: ILicense) {
this.licenseInformation = licenseInformation;
}

public start(license$: Observable<ILicense>) {
this.observable = license$;
this.subscription = this.observable.subscribe(this.updateInformation.bind(this));
}

public stop() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}

public getLicenseInformation() {
return this.licenseInformation;
}

public getLicenseInformation$() {
return this.observable;
}

public isGoldPlus() {
return (
this.licenseInformation?.isAvailable &&
this.licenseInformation?.isActive &&
this.licenseInformation?.hasAtLeast('gold')
);
}
}
2 changes: 2 additions & 0 deletions x-pack/plugins/ingest_manager/common/services/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,10 @@ export const agentRouteService = {
getEventsPath: (agentId: string) => AGENT_API_ROUTES.EVENTS_PATTERN.replace('{agentId}', agentId),
getUnenrollPath: (agentId: string) =>
AGENT_API_ROUTES.UNENROLL_PATTERN.replace('{agentId}', agentId),
getBulkUnenrollPath: () => AGENT_API_ROUTES.BULK_UNENROLL_PATTERN,
getReassignPath: (agentId: string) =>
AGENT_API_ROUTES.REASSIGN_PATTERN.replace('{agentId}', agentId),
getBulkReassignPath: () => AGENT_API_ROUTES.BULK_REASSIGN_PATTERN,
getListPath: () => AGENT_API_ROUTES.LIST_PATTERN,
getStatusPath: () => AGENT_API_ROUTES.STATUS_PATTERN,
};
Expand Down
28 changes: 28 additions & 0 deletions x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface GetAgentsRequest {
export interface GetAgentsResponse {
list: Agent[];
total: number;
totalInactive: number;
page: number;
perPage: number;
}
Expand Down Expand Up @@ -104,11 +105,24 @@ export interface PostAgentUnenrollRequest {
params: {
agentId: string;
};
body: {
force?: boolean;
};
}

// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface PostAgentUnenrollResponse {}

export interface PostBulkAgentUnenrollRequest {
body: {
agents: string[] | string;
force?: boolean;
};
}

// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface PostBulkAgentUnenrollResponse {}

export interface PutAgentReassignRequest {
params: {
agentId: string;
Expand All @@ -119,6 +133,20 @@ export interface PutAgentReassignRequest {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface PutAgentReassignResponse {}

export interface PostBulkAgentReassignRequest {
body: {
policy_id: string;
agents: string[] | string;
};
}

export interface PostBulkAgentReassignResponse {
[key: string]: {
success: boolean;
error?: Error;
};
}

export interface GetOneAgentEventsRequest {
params: {
agentId: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export { useCapabilities } from './use_capabilities';
export { useCore } from './use_core';
export { useConfig, ConfigContext } from './use_config';
export { useSetupDeps, useStartDeps, DepsContext } from './use_deps';
export { licenseService, useLicense } from './use_license';
export { useBreadcrumbs } from './use_breadcrumbs';
export { useLink } from './use_link';
export { useKibanaLink } from './use_kibana_link';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { LicenseService } from '../services';

export const licenseService = new LicenseService();

export function useLicense() {
return licenseService;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,14 @@ import {
GetOneAgentResponse,
GetOneAgentEventsResponse,
GetOneAgentEventsRequest,
PostAgentUnenrollRequest,
PostBulkAgentUnenrollRequest,
PostBulkAgentUnenrollResponse,
PostAgentUnenrollResponse,
PutAgentReassignRequest,
PutAgentReassignResponse,
PostBulkAgentReassignRequest,
PostBulkAgentReassignResponse,
GetAgentsRequest,
GetAgentsResponse,
GetAgentStatusRequest,
Expand Down Expand Up @@ -83,3 +89,40 @@ export function sendPutAgentReassign(
...options,
});
}

export function sendPostBulkAgentReassign(
body: PostBulkAgentReassignRequest['body'],
options?: RequestOptions
) {
return sendRequest<PostBulkAgentReassignResponse>({
method: 'post',
path: agentRouteService.getBulkReassignPath(),
body,
...options,
});
}

export function sendPostAgentUnenroll(
agentId: string,
body: PostAgentUnenrollRequest['body'],
options?: RequestOptions
) {
return sendRequest<PostAgentUnenrollResponse>({
path: agentRouteService.getUnenrollPath(agentId),
method: 'post',
body,
...options,
});
}

export function sendPostBulkAgentUnenroll(
body: PostBulkAgentUnenrollRequest['body'],
options?: RequestOptions
) {
return sendRequest<PostBulkAgentUnenrollResponse>({
path: agentRouteService.getBulkUnenrollPath(),
method: 'post',
body,
...options,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,16 @@ import { PAGE_ROUTING_PATHS } from './constants';
import { DefaultLayout, WithoutHeaderLayout } from './layouts';
import { Loading, Error } from './components';
import { IngestManagerOverview, EPMApp, AgentPolicyApp, FleetApp, DataStreamApp } from './sections';
import { DepsContext, ConfigContext, useConfig } from './hooks';
import {
DepsContext,
ConfigContext,
useConfig,
useCore,
sendSetup,
sendGetPermissionsCheck,
licenseService,
} from './hooks';
import { PackageInstallProvider } from './sections/epm/hooks';
import { useCore, sendSetup, sendGetPermissionsCheck } from './hooks';
import { FleetStatusProvider } from './hooks/use_fleet_status';
import './index.scss';
import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
Expand Down Expand Up @@ -279,4 +286,5 @@ export function renderApp(
export const teardownIngestManager = (coreStart: CoreStart) => {
coreStart.chrome.docTitle.reset();
coreStart.chrome.setBreadcrumbs([]);
licenseService.stop();
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { Agent } from '../../../../types';
import { useCapabilities } from '../../../../hooks';
import { ContextMenuActions } from '../../../../components';
import { AgentUnenrollProvider, AgentReassignAgentPolicyFlyout } from '../../components';
import { AgentUnenrollAgentModal, AgentReassignAgentPolicyFlyout } from '../../components';
import { useAgentRefresh } from '../hooks';

export const AgentDetailsActionMenu: React.FunctionComponent<{
Expand All @@ -20,6 +20,7 @@ export const AgentDetailsActionMenu: React.FunctionComponent<{
const hasWriteCapabilites = useCapabilities().write;
const refreshAgent = useAgentRefresh();
const [isReassignFlyoutOpen, setIsReassignFlyoutOpen] = useState(assignFlyoutOpenByDefault);
const [isUnenrollModalOpen, setIsUnenrollModalOpen] = useState(false);
const isUnenrolling = agent.status === 'unenrolling';

const onClose = useMemo(() => {
Expand All @@ -34,7 +35,20 @@ export const AgentDetailsActionMenu: React.FunctionComponent<{
<>
{isReassignFlyoutOpen && (
<EuiPortal>
<AgentReassignAgentPolicyFlyout agent={agent} onClose={onClose} />
<AgentReassignAgentPolicyFlyout agents={[agent]} onClose={onClose} />
</EuiPortal>
)}
{isUnenrollModalOpen && (
<EuiPortal>
<AgentUnenrollAgentModal
agents={[agent]}
agentCount={1}
onClose={() => {
setIsUnenrollModalOpen(false);
refreshAgent();
}}
useForceUnenroll={isUnenrolling}
/>
</EuiPortal>
)}
<ContextMenuActions
Expand All @@ -58,32 +72,28 @@ export const AgentDetailsActionMenu: React.FunctionComponent<{
>
<FormattedMessage
id="xpack.ingestManager.agentList.reassignActionText"
defaultMessage="Assign new agent policy"
defaultMessage="Assign to new policy"
/>
</EuiContextMenuItem>,
<AgentUnenrollProvider key="unenrollAgent" forceUnenroll={isUnenrolling}>
{(unenrollAgentsPrompt) => (
<EuiContextMenuItem
icon="cross"
disabled={!hasWriteCapabilites || !agent.active}
onClick={() => {
unenrollAgentsPrompt([agent.id], 1, refreshAgent);
}}
>
{isUnenrolling ? (
<FormattedMessage
id="xpack.ingestManager.agentList.forceUnenrollOneButton"
defaultMessage="Force unenroll"
/>
) : (
<FormattedMessage
id="xpack.ingestManager.agentList.unenrollOneButton"
defaultMessage="Unenroll"
/>
)}
</EuiContextMenuItem>
<EuiContextMenuItem
icon="cross"
disabled={!hasWriteCapabilites || !agent.active}
onClick={() => {
setIsUnenrollModalOpen(true);
}}
>
{isUnenrolling ? (
<FormattedMessage
id="xpack.ingestManager.agentList.forceUnenrollOneButton"
defaultMessage="Force unenroll"
/>
) : (
<FormattedMessage
id="xpack.ingestManager.agentList.unenrollOneButton"
defaultMessage="Unenroll agent"
/>
)}
</AgentUnenrollProvider>,
</EuiContextMenuItem>,
]}
/>
</>
Expand Down
Loading

0 comments on commit 4357df7

Please sign in to comment.