Skip to content

Commit

Permalink
[Fleet] Show Count of Agent Policies on Integration Details (#86916) (#…
Browse files Browse the repository at this point in the history
…88068)

* component to show count of agent policies for integration
* API route and service to return stats of package usage
  • Loading branch information
paul-tavares authored Jan 13, 2021
1 parent 8dbe468 commit bfbd584
Show file tree
Hide file tree
Showing 13 changed files with 402 additions and 21 deletions.
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

0 comments on commit bfbd584

Please sign in to comment.