Skip to content

Commit e6e60c1

Browse files
thomasneirynckclintandrewhallkibanamachine
authored
[nit][pre-req] Split EPM Home into components (#114431) (#114828)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Clint Andrew Hall <clint.hall@elastic.co> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
1 parent dc7b8fb commit e6e60c1

File tree

6 files changed

+408
-355
lines changed

6 files changed

+408
-355
lines changed

x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.stories.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ import React from 'react';
99

1010
import { action } from '@storybook/addon-actions';
1111

12-
import type { ListProps } from './package_list_grid';
12+
import type { Props } from './package_list_grid';
1313
import { PackageListGrid } from './package_list_grid';
1414

1515
export default {
1616
component: PackageListGrid,
1717
title: 'Sections/EPM/Package List Grid',
1818
};
1919

20-
type Args = Pick<ListProps, 'title' | 'isLoading' | 'showMissingIntegrationMessage'>;
20+
type Args = Pick<Props, 'title' | 'isLoading' | 'showMissingIntegrationMessage'>;
2121

2222
const args: Args = {
2323
title: 'Installed integrations',

x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {
1414
EuiLink,
1515
EuiSpacer,
1616
EuiTitle,
17-
// @ts-ignore
1817
EuiSearchBar,
1918
EuiText,
2019
} from '@elastic/eui';
@@ -29,7 +28,7 @@ import type { IntegrationCardItem } from '../../../../../../common/types/models'
2928

3029
import { PackageCard } from './package_card';
3130

32-
export interface ListProps {
31+
export interface Props {
3332
isLoading?: boolean;
3433
controls?: ReactNode;
3534
title: string;
@@ -51,7 +50,7 @@ export function PackageListGrid({
5150
setSelectedCategory,
5251
showMissingIntegrationMessage = false,
5352
callout,
54-
}: ListProps) {
53+
}: Props) {
5554
const [searchTerm, setSearchTerm] = useState(initialSearch || '');
5655
const localSearchRef = useLocalSearch(list);
5756

@@ -107,7 +106,12 @@ export function PackageListGrid({
107106
}}
108107
onChange={onQueryChange}
109108
/>
110-
{callout ? callout : null}
109+
{callout ? (
110+
<>
111+
<EuiSpacer />
112+
{callout}
113+
</>
114+
) : null}
111115
<EuiSpacer />
112116
{gridContent}
113117
{showMissingIntegrationMessage && (
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import React, { memo, useMemo } from 'react';
9+
import { useLocation, useHistory, useParams } from 'react-router-dom';
10+
import { i18n } from '@kbn/i18n';
11+
12+
import { pagePathGetters } from '../../../../constants';
13+
import {
14+
useGetCategories,
15+
useGetPackages,
16+
useBreadcrumbs,
17+
useGetAppendCustomIntegrations,
18+
useGetReplacementCustomIntegrations,
19+
useLink,
20+
} from '../../../../hooks';
21+
import { doesPackageHaveIntegrations } from '../../../../services';
22+
import type { PackageList } from '../../../../types';
23+
import { PackageListGrid } from '../../components/package_list_grid';
24+
25+
import type { CustomIntegration } from '../../../../../../../../../../src/plugins/custom_integrations/common';
26+
27+
import type { PackageListItem } from '../../../../types';
28+
29+
import type { IntegrationCategory } from '../../../../../../../../../../src/plugins/custom_integrations/common';
30+
31+
import { useMergeEprPackagesWithReplacements } from '../../../../../../hooks/use_merge_epr_with_replacements';
32+
33+
import { mergeAndReplaceCategoryCounts } from './util';
34+
import { CategoryFacets } from './category_facets';
35+
import type { CategoryFacet } from './category_facets';
36+
37+
import type { CategoryParams } from '.';
38+
import { getParams, categoryExists, mapToCard } from '.';
39+
40+
// Packages can export multiple integrations, aka `policy_templates`
41+
// In the case where packages ship >1 `policy_templates`, we flatten out the
42+
// list of packages by bringing all integrations to top-level so that
43+
// each integration is displayed as its own tile
44+
const packageListToIntegrationsList = (packages: PackageList): PackageList => {
45+
return packages.reduce((acc: PackageList, pkg) => {
46+
const { policy_templates: policyTemplates = [], ...restOfPackage } = pkg;
47+
return [
48+
...acc,
49+
restOfPackage,
50+
...(doesPackageHaveIntegrations(pkg)
51+
? policyTemplates.map((integration) => {
52+
const { name, title, description, icons } = integration;
53+
return {
54+
...restOfPackage,
55+
id: `${restOfPackage}-${name}`,
56+
integration: name,
57+
title,
58+
description,
59+
icons: icons || restOfPackage.icons,
60+
};
61+
})
62+
: []),
63+
];
64+
}, []);
65+
};
66+
67+
const title = i18n.translate('xpack.fleet.epmList.allTitle', {
68+
defaultMessage: 'Browse by category',
69+
});
70+
71+
// TODO: clintandrewhall - this component is hard to test due to the hooks, particularly those that use `http`
72+
// or `location` to load data. Ideally, we'll split this into "connected" and "pure" components.
73+
export const AvailablePackages: React.FC = memo(() => {
74+
useBreadcrumbs('integrations_all');
75+
76+
const { selectedCategory, searchParam } = getParams(
77+
useParams<CategoryParams>(),
78+
useLocation().search
79+
);
80+
81+
const history = useHistory();
82+
83+
const { getHref, getAbsolutePath } = useLink();
84+
85+
function setSelectedCategory(categoryId: string) {
86+
const url = pagePathGetters.integrations_all({
87+
category: categoryId,
88+
searchTerm: searchParam,
89+
})[1];
90+
history.push(url);
91+
}
92+
93+
function setSearchTerm(search: string) {
94+
// Use .replace so the browser's back button is not tied to single keystroke
95+
history.replace(
96+
pagePathGetters.integrations_all({ category: selectedCategory, searchTerm: search })[1]
97+
);
98+
}
99+
100+
const { data: allCategoryPackagesRes, isLoading: isLoadingAllPackages } = useGetPackages({
101+
category: '',
102+
});
103+
104+
const { data: categoryPackagesRes, isLoading: isLoadingCategoryPackages } = useGetPackages({
105+
category: selectedCategory,
106+
});
107+
108+
const { data: categoriesRes, isLoading: isLoadingCategories } = useGetCategories({
109+
include_policy_templates: true,
110+
});
111+
112+
const eprPackages = useMemo(
113+
() => packageListToIntegrationsList(categoryPackagesRes?.response || []),
114+
[categoryPackagesRes]
115+
);
116+
117+
const allEprPackages = useMemo(
118+
() => packageListToIntegrationsList(allCategoryPackagesRes?.response || []),
119+
[allCategoryPackagesRes]
120+
);
121+
122+
const { value: replacementCustomIntegrations } = useGetReplacementCustomIntegrations();
123+
124+
const mergedEprPackages: Array<PackageListItem | CustomIntegration> =
125+
useMergeEprPackagesWithReplacements(
126+
eprPackages || [],
127+
replacementCustomIntegrations || [],
128+
selectedCategory as IntegrationCategory
129+
);
130+
131+
const { loading: isLoadingAppendCustomIntegrations, value: appendCustomIntegrations } =
132+
useGetAppendCustomIntegrations();
133+
134+
const filteredAddableIntegrations = appendCustomIntegrations
135+
? appendCustomIntegrations.filter((integration: CustomIntegration) => {
136+
if (!selectedCategory) {
137+
return true;
138+
}
139+
return integration.categories.indexOf(selectedCategory as IntegrationCategory) >= 0;
140+
})
141+
: [];
142+
143+
const eprAndCustomPackages: Array<CustomIntegration | PackageListItem> = [
144+
...mergedEprPackages,
145+
...filteredAddableIntegrations,
146+
];
147+
148+
eprAndCustomPackages.sort((a, b) => {
149+
return a.title.localeCompare(b.title);
150+
});
151+
152+
const categories = useMemo(() => {
153+
const eprAndCustomCategories: CategoryFacet[] =
154+
isLoadingCategories ||
155+
isLoadingAppendCustomIntegrations ||
156+
!appendCustomIntegrations ||
157+
!categoriesRes
158+
? []
159+
: mergeAndReplaceCategoryCounts(
160+
categoriesRes.response as CategoryFacet[],
161+
appendCustomIntegrations
162+
);
163+
164+
return [
165+
{
166+
id: '',
167+
count: (allEprPackages?.length || 0) + (appendCustomIntegrations?.length || 0),
168+
},
169+
...(eprAndCustomCategories ? eprAndCustomCategories : []),
170+
] as CategoryFacet[];
171+
}, [
172+
allEprPackages?.length,
173+
appendCustomIntegrations,
174+
categoriesRes,
175+
isLoadingAppendCustomIntegrations,
176+
isLoadingCategories,
177+
]);
178+
179+
if (!isLoadingCategories && !categoryExists(selectedCategory, categories)) {
180+
history.replace(pagePathGetters.integrations_all({ category: '', searchTerm: searchParam })[1]);
181+
return null;
182+
}
183+
184+
const controls = categories ? (
185+
<CategoryFacets
186+
showCounts={false}
187+
isLoading={isLoadingCategories || isLoadingAllPackages || isLoadingAppendCustomIntegrations}
188+
categories={categories}
189+
selectedCategory={selectedCategory}
190+
onCategoryChange={({ id }: CategoryFacet) => {
191+
setSelectedCategory(id);
192+
}}
193+
/>
194+
) : null;
195+
196+
const cards = eprAndCustomPackages.map((item) => {
197+
return mapToCard(getAbsolutePath, getHref, item);
198+
});
199+
200+
return (
201+
<PackageListGrid
202+
isLoading={isLoadingCategoryPackages}
203+
title={title}
204+
controls={controls}
205+
initialSearch={searchParam}
206+
list={cards}
207+
setSelectedCategory={setSelectedCategory}
208+
onSearchChange={setSearchTerm}
209+
showMissingIntegrationMessage
210+
/>
211+
);
212+
});

x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/category_facets.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,21 @@ interface ALL_CATEGORY {
2121

2222
export type CategoryFacet = IntegrationCategoryCount | ALL_CATEGORY;
2323

24+
export interface Props {
25+
showCounts: boolean;
26+
isLoading?: boolean;
27+
categories: CategoryFacet[];
28+
selectedCategory: string;
29+
onCategoryChange: (category: CategoryFacet) => unknown;
30+
}
31+
2432
export function CategoryFacets({
2533
showCounts,
2634
isLoading,
2735
categories,
2836
selectedCategory,
2937
onCategoryChange,
30-
}: {
31-
showCounts: boolean;
32-
isLoading?: boolean;
33-
categories: CategoryFacet[];
34-
selectedCategory: string;
35-
onCategoryChange: (category: CategoryFacet) => unknown;
36-
}) {
38+
}: Props) {
3739
const controls = (
3840
<EuiFacetGroup>
3941
{isLoading ? (

0 commit comments

Comments
 (0)