Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[7.x] [Fleet] Show Count of Agent Policies on Integration Details (#86916) #88068

Merged
merged 1 commit into from
Jan 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions x-pack/plugins/fleet/common/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const EPM_API_ROUTES = {
DELETE_PATTERN: EPM_PACKAGES_ONE,
FILEPATH_PATTERN: `${EPM_PACKAGES_FILE}/{filePath*}`,
CATEGORIES_PATTERN: `${EPM_API_ROOT}/categories`,
STATS_PATTERN: `${EPM_PACKAGES_MANY}/{pkgName}/stats`,
};

// Data stream API routes
Expand Down
4 changes: 4 additions & 0 deletions x-pack/plugins/fleet/common/services/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ export const epmRouteService = {
return EPM_API_ROUTES.INFO_PATTERN.replace('{pkgkey}', pkgkey);
},

getStatsPath: (pkgName: string) => {
return EPM_API_ROUTES.STATS_PATTERN.replace('{pkgName}', pkgName);
},

getFilePath: (filePath: string) => {
return `${EPM_API_ROOT}${filePath.replace('/package', '/packages')}`;
},
Expand Down
4 changes: 4 additions & 0 deletions x-pack/plugins/fleet/common/types/models/epm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,10 @@ export interface Installation extends SavedObjectAttributes {
install_source: InstallSource;
}

export interface PackageUsageStats {
agent_policy_count: number;
}

export type Installable<T> = Installed<T> | NotInstalled<T>;

export type Installed<T = {}> = T & {
Expand Down
11 changes: 11 additions & 0 deletions x-pack/plugins/fleet/common/types/rest_spec/epm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Installable,
RegistrySearchResult,
PackageInfo,
PackageUsageStats,
} from '../models/epm';

export interface GetCategoriesRequest {
Expand Down Expand Up @@ -54,6 +55,16 @@ export interface GetInfoResponse {
response: PackageInfo;
}

export interface GetStatsRequest {
params: {
pkgname: string;
};
}

export interface GetStatsResponse {
response: PackageUsageStats;
}

export interface InstallPackageRequest {
params: {
pkgkey: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
InstallPackageResponse,
DeletePackageResponse,
} from '../../types';
import { GetStatsResponse } from '../../../../../common';

export const useGetCategories = (query: GetCategoriesRequest['query'] = {}) => {
return useRequest<GetCategoriesResponse>({
Expand Down Expand Up @@ -47,6 +48,13 @@ export const useGetPackageInfoByKey = (pkgkey: string) => {
});
};

export const useGetPackageStats = (pkgName: string) => {
return useRequest<GetStatsResponse>({
path: epmRouteService.getStatsPath(pkgName),
method: 'get',
});
};

export const sendGetPackageInfoByKey = (pkgkey: string) => {
return sendRequest<GetInfoResponse>({
path: epmRouteService.getInfoPath(pkgkey),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
GetFleetStatusResponse,
GetInfoResponse,
GetPackagePoliciesResponse,
GetStatsResponse,
} from '../../../../../../../common/types/rest_spec';
import { DetailViewPanelName, KibanaAssetType } from '../../../../../../../common/types/models';
import {
Expand All @@ -29,7 +30,7 @@ describe('when on integration detail', () => {
const detailPageUrlPath = pagePathGetters.integration_details({ pkgkey });
let testRenderer: TestRenderer;
let renderResult: ReturnType<typeof testRenderer.render>;
let mockedApi: MockedApi;
let mockedApi: MockedApi<EpmPackageDetailsResponseProvidersMock>;
const render = () =>
(renderResult = testRenderer.render(
<Route path={PAGE_ROUTING_PATHS.integration_details}>
Expand All @@ -48,6 +49,39 @@ describe('when on integration detail', () => {
window.location.hash = '#/';
});

describe('and the package is installed', () => {
beforeEach(() => render());

it('should display agent policy usage count', async () => {
await mockedApi.waitForApi();
expect(renderResult.queryByTestId('agentPolicyCount')).not.toBeNull();
});

it('should show the Policies tab', async () => {
await mockedApi.waitForApi();
expect(renderResult.queryByTestId('tab-policies')).not.toBeNull();
});
});

describe('and the package is not installed', () => {
beforeEach(() => {
const unInstalledPackage = mockedApi.responseProvider.epmGetInfo();
unInstalledPackage.response.status = 'not_installed';
mockedApi.responseProvider.epmGetInfo.mockReturnValue(unInstalledPackage);
render();
});

it('should NOT display agent policy usage count', async () => {
await mockedApi.waitForApi();
expect(renderResult.queryByTestId('agentPolicyCount')).toBeNull();
});

it('should NOT the Policies tab', async () => {
await mockedApi.waitForApi();
expect(renderResult.queryByTestId('tab-policies')).toBeNull();
});
});

describe('and a custom UI extension is NOT registered', () => {
beforeEach(() => render());

Expand Down Expand Up @@ -190,12 +224,27 @@ describe('when on integration detail', () => {
});
});

interface MockedApi {
interface MockedApi<
R extends Record<string, jest.MockedFunction<any>> = Record<string, jest.MockedFunction<any>>
> {
/** Will return a promise that resolves when triggered APIs are complete */
waitForApi: () => Promise<void>;
/** A object containing the list of API response provider functions that are used by the mocked API */
responseProvider: R;
}

const mockApiCalls = (http: MockedFleetStartServices['http']): MockedApi => {
interface EpmPackageDetailsResponseProvidersMock {
epmGetInfo: jest.MockedFunction<() => GetInfoResponse>;
epmGetFile: jest.MockedFunction<() => string>;
epmGetStats: jest.MockedFunction<() => GetStatsResponse>;
fleetSetup: jest.MockedFunction<() => GetFleetStatusResponse>;
packagePolicyList: jest.MockedFunction<() => GetPackagePoliciesResponse>;
agentPolicyList: jest.MockedFunction<() => GetAgentPoliciesResponse>;
}

const mockApiCalls = (
http: MockedFleetStartServices['http']
): MockedApi<EpmPackageDetailsResponseProvidersMock> => {
let inflightApiCalls = 0;
const apiDoneListeners: Array<() => void> = [];
const markApiCallAsHandled = async () => {
Expand Down Expand Up @@ -663,31 +712,62 @@ On Windows, the module was tested with Nginx installed from the Chocolatey repos
perPage: 100,
};

const epmGetStatsResponse: GetStatsResponse = {
response: {
agent_policy_count: 2,
},
};

const mockedApiInterface: MockedApi<EpmPackageDetailsResponseProvidersMock> = {
waitForApi() {
return new Promise((resolve) => {
if (inflightApiCalls > 0) {
apiDoneListeners.push(resolve);
} else {
resolve();
}
});
},
responseProvider: {
epmGetInfo: jest.fn().mockReturnValue(epmPackageResponse),
epmGetFile: jest.fn().mockReturnValue(packageReadMe),
epmGetStats: jest.fn().mockReturnValue(epmGetStatsResponse),
fleetSetup: jest.fn().mockReturnValue(agentsSetupResponse),
packagePolicyList: jest.fn().mockReturnValue(packagePoliciesResponse),
agentPolicyList: jest.fn().mockReturnValue(agentPoliciesResponse),
},
};

http.get.mockImplementation(async (path) => {
if (typeof path === 'string') {
if (path === epmRouteService.getInfoPath(`nginx-0.3.7`)) {
markApiCallAsHandled();
return epmPackageResponse;
return mockedApiInterface.responseProvider.epmGetInfo();
}

if (path === epmRouteService.getFilePath('/package/nginx/0.3.7/docs/README.md')) {
markApiCallAsHandled();
return packageReadMe;
return mockedApiInterface.responseProvider.epmGetFile();
}

if (path === fleetSetupRouteService.getFleetSetupPath()) {
markApiCallAsHandled();
return agentsSetupResponse;
return mockedApiInterface.responseProvider.fleetSetup();
}

if (path === packagePolicyRouteService.getListPath()) {
markApiCallAsHandled();
return packagePoliciesResponse;
return mockedApiInterface.responseProvider.packagePolicyList();
}

if (path === agentPolicyRouteService.getListPath()) {
markApiCallAsHandled();
return agentPoliciesResponse;
return mockedApiInterface.responseProvider.agentPolicyList();
}

if (path === epmRouteService.getStatsPath('nginx')) {
markApiCallAsHandled();
return mockedApiInterface.responseProvider.epmGetStats();
}

const err = new Error(`API [GET ${path}] is not MOCKED!`);
Expand All @@ -697,15 +777,5 @@ On Windows, the module was tested with Nginx installed from the Chocolatey repos
}
});

return {
waitForApi() {
return new Promise((resolve) => {
if (inflightApiCalls > 0) {
apiDoneListeners.push(resolve);
} else {
resolve();
}
});
},
};
return mockedApiInterface;
};
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import './index.scss';
import { useUIExtension } from '../../../../hooks/use_ui_extension';
import { PLUGIN_ID } from '../../../../../../../common/constants';
import { pkgKeyFromPackageInfo } from '../../../../services/pkg_key_from_package_info';
import { IntegrationAgentPolicyCount } from './integration_agent_policy_count';

export const DEFAULT_PANEL: DetailViewPanelName = 'overview';

Expand Down Expand Up @@ -239,6 +240,18 @@ export function Detail() {
</EuiFlexGroup>
),
},
...(packageInstallStatus === 'installed'
? [
{ isDivider: true },
{
label: i18n.translate('xpack.fleet.epm.usedByLabel', {
defaultMessage: 'Agent Policies',
}),
'data-test-subj': 'agentPolicyCount',
content: <IntegrationAgentPolicyCount packageName={packageInfo.name} />,
},
]
: []),
{ isDivider: true },
{
content: (
Expand All @@ -264,7 +277,7 @@ export function Detail() {
),
},
].map((item, index) => (
<EuiFlexItem grow={false} key={index}>
<EuiFlexItem grow={false} key={index} data-test-subj={item['data-test-subj']}>
{item.isDivider ?? false ? (
<Divider />
) : item.label ? (
Expand All @@ -285,6 +298,7 @@ export function Detail() {
handleAddIntegrationPolicyClick,
hasWriteCapabilites,
packageInfo,
packageInstallStatus,
pkgkey,
updateAvailable,
]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* 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 React, { memo } from 'react';
import { useGetPackageStats } from '../../../../hooks';

/**
* Displays a count of Agent Policies that are using the given integration
*/
export const IntegrationAgentPolicyCount = memo<{ packageName: string }>(({ packageName }) => {
const { data } = useGetPackageStats(packageName);

return <>{data?.response.agent_policy_count ?? 0}</>;
});
20 changes: 20 additions & 0 deletions x-pack/plugins/fleet/server/routes/epm/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
BulkInstallPackageInfo,
BulkInstallPackagesResponse,
IBulkInstallPackageHTTPError,
GetStatsResponse,
} from '../../../common';
import {
GetCategoriesRequestSchema,
Expand All @@ -27,6 +28,7 @@ import {
InstallPackageByUploadRequestSchema,
DeletePackageRequestSchema,
BulkUpgradePackagesFromRegistryRequestSchema,
GetStatsRequestSchema,
} from '../../types';
import {
BulkInstallResponse,
Expand All @@ -48,6 +50,7 @@ import { splitPkgKey } from '../../services/epm/registry';
import { licenseService } from '../../services';
import { getArchiveEntry } from '../../services/epm/archive/cache';
import { getAsset } from '../../services/epm/archive/storage';
import { getPackageUsageStats } from '../../services/epm/packages/get';

export const getCategoriesHandler: RequestHandler<
undefined,
Expand Down Expand Up @@ -196,6 +199,23 @@ export const getInfoHandler: RequestHandler<TypeOf<typeof GetInfoRequestSchema.p
}
};

export const getStatsHandler: RequestHandler<TypeOf<typeof GetStatsRequestSchema.params>> = async (
context,
request,
response
) => {
try {
const { pkgName } = request.params;
const savedObjectsClient = context.core.savedObjects.client;
const body: GetStatsResponse = {
response: await getPackageUsageStats({ savedObjectsClient, pkgName }),
};
return response.ok({ body });
} catch (error) {
return defaultIngestErrorHandler({ error, response });
}
};

export const installPackageFromRegistryHandler: RequestHandler<
TypeOf<typeof InstallPackageFromRegistryRequestSchema.params>,
undefined,
Expand Down
11 changes: 11 additions & 0 deletions x-pack/plugins/fleet/server/routes/epm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
installPackageByUploadHandler,
deletePackageHandler,
bulkInstallPackagesFromRegistryHandler,
getStatsHandler,
} from './handlers';
import {
GetCategoriesRequestSchema,
Expand All @@ -25,6 +26,7 @@ import {
InstallPackageByUploadRequestSchema,
DeletePackageRequestSchema,
BulkUpgradePackagesFromRegistryRequestSchema,
GetStatsRequestSchema,
} from '../../types';

const MAX_FILE_SIZE_BYTES = 104857600; // 100MB
Expand Down Expand Up @@ -57,6 +59,15 @@ export const registerRoutes = (router: IRouter) => {
getLimitedListHandler
);

router.get(
{
path: EPM_API_ROUTES.STATS_PATTERN,
validate: GetStatsRequestSchema,
options: { tags: [`access:${PLUGIN_ID}`] },
},
getStatsHandler
);

router.get(
{
path: EPM_API_ROUTES.FILEPATH_PATTERN,
Expand Down
Loading