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

[Fleet] Show Count of Agent Policies on Integration Details #86916

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8f209ef
component to show count of agent policies for integration
paul-tavares Dec 22, 2020
1796a5d
front end setup for retrieving package summary from an api
paul-tavares Dec 22, 2020
1e7ce9f
API route registration and setup for summary stats
paul-tavares Dec 22, 2020
58b0d39
service to return summary of package usage
paul-tavares Dec 22, 2020
874998c
Change display of count on UI
paul-tavares Dec 23, 2020
9f2bb2a
Tests for getPackageUsageSummary() service
paul-tavares Dec 23, 2020
ffb866d
Adjust test case
paul-tavares Dec 23, 2020
c0f04d6
Merge remote-tracking branch 'upstream/master' into task/fleet-86369-…
paul-tavares Dec 23, 2020
a3240ab
UI tests for when package in installed and uninstalled + refacror
paul-tavares Dec 23, 2020
d4e9dd9
Merge remote-tracking branch 'upstream/master' into task/fleet-86369-…
paul-tavares Jan 6, 2021
6da3349
rename API endpoint to `/stats`
paul-tavares Jan 6, 2021
89f49eb
Correct test label
paul-tavares Jan 7, 2021
b67afc1
Rename several types and methods (from `*summary*` to `*stats*`)
paul-tavares Jan 7, 2021
c30a3c1
Fix mock api response
paul-tavares Jan 7, 2021
87daa10
Merge remote-tracking branch 'upstream/master' into task/fleet-86369-…
paul-tavares Jan 7, 2021
3eefff1
Merge branch 'master' into task/fleet-86369-add-used-by-counts
kibanamachine Jan 11, 2021
13604f0
Refactored var names/types from `*Summary` to `*Stats`
paul-tavares Jan 11, 2021
3b4d481
Merge remote-tracking branch 'origin/task/fleet-86369-add-used-by-cou…
paul-tavares Jan 11, 2021
9bcd2d8
Merge remote-tracking branch 'upstream/master' into task/fleet-86369-…
paul-tavares Jan 11, 2021
4c3d739
Merge remote-tracking branch 'upstream/master' into task/fleet-86369-…
paul-tavares Jan 11, 2021
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`,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be called summary elsewhere. Perhaps we should make the var names consistent, to mentally correlate them a bit better.

If the route already exists and is /stats, I can see giving the route's var name the same as the route itself. But then it's switched to called summary on the layer right outside that. Consider naming the pattern var name summary as well. Reduces the mental context switch to just here, next to the route, which is a little more concrete to reason about.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have renamed everything else *Stats* in order keep consistent with this decision here 😄 . Thanks for highlighting that - "me culpa" 😞

};

// 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<
Copy link
Contributor Author

@paul-tavares paul-tavares Dec 23, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of this here is refactoring which will facilitate breaking this out to a separate set of test utilities so that this approach can be used with other test areas of Fleet.
The mocked api interface could now also return the set of functions that are used to fulfill the API calls, which allow each test case to adjust them (if necessary) before rendering the UI. An example of this can be seen above on line 69-70

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