mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[Fleet] Implement subcategories in integrations UI (#148894)
## Summary Closes https://github.com/elastic/kibana/issues/147125 Closes https://github.com/elastic/kibana/issues/148654 - Implement subcategories in integrations UI. - I also refactored the `AvailablePackages`, `InstalledPackages` and `PackageListGrid` components to make easier to do changes. I extracted the logic from `AvailablePackages` in a hook `useAvailablePackages` and updated it. - Sneaked a small bugfix into it. It's in commit7b2946fe3b
: removed the js logic that handled the sticky sidemenu, that would cause flickering in some cases and implemented it with some css instead. ## Repro steps ### Display actual subcategories coming from EPR - Enable `showIntegrationsSubcategories` feature flag; - Navigate to Integrations page. `Security` and `infrastructure` categories have sub categories. - When clicking on a subcategory, the page URL gets updated. For example, if clicking on `Infrastructure` > `Kubernetes`, the url will be `/integrations/browse/infrastructure/kubernetes`. It works with search too, if searching `abc` in the subcategory the url will update to `/integrations/browse/infrastructure/kubernetes?q=abc` ### Display mock data Same repro steps as before, but I added some mock subcategories: Added the following subcategories file <details> <p> ``` export const mockSubcategories = [ { id: 'kubernetes', title: 'Kubernetes', count: 18, parent_id: 'infrastructure', parent_title: 'Infrastructure', }, { id: 'message_queue', title: 'Message Broker', count: 7, parent_id: 'infrastructure', parent_title: 'Infrastructure', }, { id: 'monitoring', title: 'Monitoring', count: 16, parent_id: 'observability', parent_title: 'Observability', }, { id: 'threat_intel', title: 'Threat Intelligence', count: 10, parent_id: 'security', parent_title: 'Security', }, { id: 'web', title: 'Web Server', count: 36, parent_id: 'infrastructure', parent_title: 'Infrastructure', }, { id: 'security_mock_1', title: 'Security mock 1', count: 10, parent_id: 'security', parent_title: 'Security', }, { id: 'infrastructure_mock_1', title: 'Subcategory 1', count: 12, parent_id: 'infrastructure', parent_title: 'Infrastructure', }, { id: 'infrastructure_mock_2', title: 'Subcategory 2', count: 12, parent_id: 'infrastructure', parent_title: 'Infrastructure', }, { id: 'infrastructure_mock_3', title: 'Subcategory 3', count: 12, parent_id: 'infrastructure', parent_title: 'Infrastructure', }, { id: 'infrastructure_mock_4', title: 'Subcategory 4', count: 12, parent_id: 'infrastructure', parent_title: 'Infrastructure', }, { id: 'infrastructure_mock_5', title: 'Subcategory 5', count: 12, parent_id: 'infrastructure', parent_title: 'Infrastructure', }, { id: 'infrastructure_mock_6', title: 'Subcategory 6', count: 12, parent_id: 'infrastructure', parent_title: 'Infrastructure', }, ]; ``` </p> </details> I then imported it and replaced the subcategories here:7b2946fe3b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/hooks/use_available_packages.tsx (L226)
. It should provide more subcategories under `infrastructure`. ### Screenshots If subcategories are up to 6, they get rendered on top of the integrations page: <img width="1679" alt="Screenshot 2023-01-17 at 16 11 01" src="https://user-images.githubusercontent.com/16084106/212963949-6e1fdccc-872d-4c40-a594-905e34218494.png"> If more the 6 subcategories are present, the remaining ones get hidden under an option button: <img width="1593" alt="Screenshot 2023-01-17 at 16 07 27" src="https://user-images.githubusercontent.com/16084106/212963863-0be8a97f-75ba-4bfb-a28c-bb9bbeb4c8b3.png"> <img width="1693" alt="Screenshot 2023-01-17 at 16 07 41" src="https://user-images.githubusercontent.com/16084106/212963899-2846b292-45c2-4fc2-91ca-e947653ec961.png"> ### Checklist - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
f83a60c953
commit
fabfb43740
11 changed files with 661 additions and 373 deletions
|
@ -17,6 +17,7 @@ export const allowedExperimentalValues = Object.freeze({
|
|||
showDevtoolsRequest: true,
|
||||
diagnosticFileUploadEnabled: false,
|
||||
experimentalDataStreamSettings: false,
|
||||
showIntegrationsSubcategories: false,
|
||||
});
|
||||
|
||||
type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;
|
||||
|
|
|
@ -289,6 +289,8 @@ export interface CategorySummaryItem {
|
|||
id: CategoryId;
|
||||
title: string;
|
||||
count: number;
|
||||
parent_id?: string;
|
||||
parent_title?: string;
|
||||
}
|
||||
|
||||
export type RequirementsByServiceName = PackageSpecManifest['conditions'];
|
||||
|
|
|
@ -172,7 +172,8 @@ describe('Add Integration - Real API', () => {
|
|||
|
||||
it('should filter integrations by category', () => {
|
||||
setupIntegrations();
|
||||
cy.getBySel(getIntegrationCategories('aws')).click();
|
||||
cy.getBySel(getIntegrationCategories('aws')).click({ scrollBehavior: false });
|
||||
|
||||
cy.getBySel(INTEGRATIONS_SEARCHBAR.BADGE).contains('AWS').should('exist');
|
||||
cy.getBySel(INTEGRATION_LIST).find('.euiCard').should('have.length.greaterThan', 29);
|
||||
|
||||
|
|
|
@ -45,10 +45,13 @@ const categories = [
|
|||
export const EmptyList = (props: Args) => (
|
||||
<PackageListGrid
|
||||
list={[]}
|
||||
onSearchChange={action('onSearchChange')}
|
||||
setSelectedCategory={action('setSelectedCategory')}
|
||||
searchTerm=""
|
||||
setSearchTerm={action('setSearchTerm')}
|
||||
setCategory={action('setCategory')}
|
||||
categories={categories}
|
||||
selectedCategory=""
|
||||
setUrlandReplaceHistory={action('setUrlandReplaceHistory')}
|
||||
setUrlandPushHistory={action('setUrlandPushHistory')}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
@ -129,10 +132,13 @@ export const List = (props: Args) => (
|
|||
categories: ['category_two'],
|
||||
},
|
||||
]}
|
||||
onSearchChange={action('onSearchChange')}
|
||||
setSelectedCategory={action('setSelectedCategory')}
|
||||
searchTerm=""
|
||||
setSearchTerm={action('setSearchTerm')}
|
||||
setCategory={action('setCategory')}
|
||||
categories={categories}
|
||||
selectedCategory=""
|
||||
setUrlandReplaceHistory={action('setUrlandReplaceHistory')}
|
||||
setUrlandPushHistory={action('setUrlandPushHistory')}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import type { ReactNode, FunctionComponent } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import React, { useCallback, useState, useRef, useEffect } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import {
|
||||
|
@ -22,6 +22,11 @@ import {
|
|||
useEuiTheme,
|
||||
EuiIcon,
|
||||
EuiScreenReaderOnly,
|
||||
EuiButton,
|
||||
EuiButtonIcon,
|
||||
EuiPopover,
|
||||
EuiContextMenuPanel,
|
||||
EuiContextMenuItem,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -35,6 +40,10 @@ import type { IntegrationCardItem } from '../../../../../../common/types/models'
|
|||
|
||||
import type { ExtendedIntegrationCategory, CategoryFacet } from '../screens/home/category_facets';
|
||||
|
||||
import type { IntegrationsURLParameters } from '../screens/home/hooks/use_available_packages';
|
||||
|
||||
import { ExperimentalFeaturesService } from '../../../services';
|
||||
|
||||
import { promoteFeaturedIntegrations } from './utils';
|
||||
|
||||
import { PackageCard } from './package_card';
|
||||
|
@ -42,16 +51,22 @@ import { PackageCard } from './package_card';
|
|||
export interface Props {
|
||||
isLoading?: boolean;
|
||||
controls?: ReactNode | ReactNode[];
|
||||
title?: string;
|
||||
list: IntegrationCardItem[];
|
||||
initialSearch?: string;
|
||||
searchTerm: string;
|
||||
setSearchTerm: (search: string) => void;
|
||||
selectedCategory: ExtendedIntegrationCategory;
|
||||
setSelectedCategory: (category: string) => void;
|
||||
setCategory: (category: ExtendedIntegrationCategory) => void;
|
||||
categories: CategoryFacet[];
|
||||
onSearchChange: (search: string) => void;
|
||||
showMissingIntegrationMessage?: boolean;
|
||||
setUrlandReplaceHistory: (params: IntegrationsURLParameters) => void;
|
||||
setUrlandPushHistory: (params: IntegrationsURLParameters) => void;
|
||||
callout?: JSX.Element | null;
|
||||
// Props used only in AvailablePackages component:
|
||||
showCardLabels?: boolean;
|
||||
title?: string;
|
||||
availableSubCategories?: CategoryFacet[];
|
||||
selectedSubCategory?: string;
|
||||
setSelectedSubCategory?: (c: string | undefined) => void;
|
||||
showMissingIntegrationMessage?: boolean;
|
||||
}
|
||||
|
||||
export const PackageListGrid: FunctionComponent<Props> = ({
|
||||
|
@ -59,43 +74,63 @@ export const PackageListGrid: FunctionComponent<Props> = ({
|
|||
controls,
|
||||
title,
|
||||
list,
|
||||
initialSearch,
|
||||
onSearchChange,
|
||||
searchTerm,
|
||||
setSearchTerm,
|
||||
selectedCategory,
|
||||
setSelectedCategory,
|
||||
setCategory,
|
||||
categories,
|
||||
availableSubCategories,
|
||||
setSelectedSubCategory,
|
||||
selectedSubCategory,
|
||||
setUrlandReplaceHistory,
|
||||
setUrlandPushHistory,
|
||||
showMissingIntegrationMessage = false,
|
||||
callout,
|
||||
showCardLabels = true,
|
||||
}) => {
|
||||
const [searchTerm, setSearchTerm] = useState(initialSearch || '');
|
||||
const localSearchRef = useLocalSearch(list);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const [isSticky, setIsSticky] = useState(false);
|
||||
const [windowScrollY] = useState(window.scrollY);
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
useEffect(() => {
|
||||
const menuRefCurrent = menuRef.current;
|
||||
const onScroll = () => {
|
||||
if (menuRefCurrent) {
|
||||
setIsSticky(menuRefCurrent?.getBoundingClientRect().top < 110);
|
||||
}
|
||||
};
|
||||
window.addEventListener('scroll', onScroll);
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, [windowScrollY, isSticky]);
|
||||
const [isPopoverOpen, setPopover] = useState(false);
|
||||
|
||||
const MAX_SUBCATEGORIES_NUMBER = 6;
|
||||
|
||||
const { showIntegrationsSubcategories } = ExperimentalFeaturesService.get();
|
||||
|
||||
const onButtonClick = () => {
|
||||
setPopover(!isPopoverOpen);
|
||||
};
|
||||
|
||||
const closePopover = () => {
|
||||
setPopover(false);
|
||||
};
|
||||
|
||||
const onQueryChange = (e: any) => {
|
||||
const queryText = e.target.value;
|
||||
setSearchTerm(queryText);
|
||||
onSearchChange(queryText);
|
||||
setUrlandReplaceHistory({
|
||||
searchString: queryText,
|
||||
categoryId: selectedCategory,
|
||||
subCategoryId: selectedSubCategory,
|
||||
});
|
||||
};
|
||||
|
||||
const resetQuery = () => {
|
||||
setSearchTerm('');
|
||||
setUrlandReplaceHistory({ searchString: '', categoryId: '', subCategoryId: '' });
|
||||
};
|
||||
|
||||
const onSubCategoryClick = useCallback(
|
||||
(subCategory: string) => {
|
||||
if (setSelectedSubCategory) setSelectedSubCategory(subCategory);
|
||||
setUrlandPushHistory({
|
||||
categoryId: selectedCategory,
|
||||
subCategoryId: subCategory,
|
||||
});
|
||||
},
|
||||
[selectedCategory, setSelectedSubCategory, setUrlandPushHistory]
|
||||
);
|
||||
|
||||
const selectedCategoryTitle = selectedCategory
|
||||
? categories.find((category) => category.id === selectedCategory)?.title
|
||||
: undefined;
|
||||
|
@ -113,116 +148,199 @@ export const PackageListGrid: FunctionComponent<Props> = ({
|
|||
return promoteFeaturedIntegrations(filteredList, selectedCategory);
|
||||
}, [isLoading, list, localSearchRef, searchTerm, selectedCategory]);
|
||||
|
||||
const controlsContent = <ControlsColumn title={title} controls={controls} sticky={isSticky} />;
|
||||
let gridContent: JSX.Element;
|
||||
const splitSubcategories = (
|
||||
subcategories: CategoryFacet[] | undefined
|
||||
): { visibleSubCategories?: CategoryFacet[]; hiddenSubCategories?: CategoryFacet[] } => {
|
||||
if (!subcategories) return {};
|
||||
else if (subcategories && subcategories?.length < MAX_SUBCATEGORIES_NUMBER) {
|
||||
return { visibleSubCategories: subcategories, hiddenSubCategories: [] };
|
||||
} else if (subcategories && subcategories?.length >= MAX_SUBCATEGORIES_NUMBER) {
|
||||
return {
|
||||
visibleSubCategories: subcategories.slice(0, MAX_SUBCATEGORIES_NUMBER),
|
||||
hiddenSubCategories: subcategories.slice(MAX_SUBCATEGORIES_NUMBER),
|
||||
};
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
if (isLoading || !localSearchRef.current) {
|
||||
gridContent = <Loading />;
|
||||
} else {
|
||||
gridContent = (
|
||||
<GridColumn
|
||||
list={filteredPromotedList}
|
||||
showMissingIntegrationMessage={showMissingIntegrationMessage}
|
||||
showCardLabels={showCardLabels}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const splitSubcat = splitSubcategories(availableSubCategories);
|
||||
const { visibleSubCategories } = splitSubcat;
|
||||
const hiddenSubCategoriesItems = useMemo(() => {
|
||||
return splitSubcat?.hiddenSubCategories?.map((subCategory) => {
|
||||
return (
|
||||
<EuiContextMenuItem
|
||||
key={subCategory.id}
|
||||
onClick={() => {
|
||||
onSubCategoryClick(subCategory.id);
|
||||
closePopover();
|
||||
}}
|
||||
>
|
||||
{subCategory.title}
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
});
|
||||
}, [onSubCategoryClick, splitSubcat.hiddenSubCategories]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={menuRef}>
|
||||
<EuiFlexGroup
|
||||
alignItems="flexStart"
|
||||
gutterSize="xl"
|
||||
data-test-subj="epmList.integrationCards"
|
||||
>
|
||||
<EuiFlexItem grow={1} className={isSticky ? 'kbnStickyMenu' : ''}>
|
||||
{controlsContent}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={5}>
|
||||
<EuiFieldSearch
|
||||
data-test-subj="epmList.searchBar"
|
||||
placeholder={i18n.translate('xpack.fleet.epmList.searchPackagesPlaceholder', {
|
||||
defaultMessage: 'Search for integrations',
|
||||
})}
|
||||
value={searchTerm}
|
||||
onChange={(e) => onQueryChange(e)}
|
||||
isClearable={true}
|
||||
incremental={true}
|
||||
fullWidth={true}
|
||||
prepend={
|
||||
selectedCategoryTitle ? (
|
||||
<EuiText
|
||||
data-test-subj="epmList.categoryBadge"
|
||||
size="xs"
|
||||
<EuiFlexGroup alignItems="flexStart" gutterSize="xl" data-test-subj="epmList.integrationCards">
|
||||
<EuiFlexItem
|
||||
data-test-subj="epmList.controlsSideColumn"
|
||||
grow={1}
|
||||
style={{
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 100,
|
||||
}}
|
||||
>
|
||||
<ControlsColumn controls={controls} title={title} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={5} data-test-subj="epmList.mainColumn">
|
||||
<EuiFieldSearch
|
||||
data-test-subj="epmList.searchBar"
|
||||
placeholder={i18n.translate('xpack.fleet.epmList.searchPackagesPlaceholder', {
|
||||
defaultMessage: 'Search for integrations',
|
||||
})}
|
||||
value={searchTerm}
|
||||
onChange={(e) => onQueryChange(e)}
|
||||
isClearable={true}
|
||||
incremental={true}
|
||||
fullWidth={true}
|
||||
prepend={
|
||||
selectedCategoryTitle ? (
|
||||
<EuiText
|
||||
data-test-subj="epmList.categoryBadge"
|
||||
size="xs"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontWeight: euiTheme.font.weight.bold,
|
||||
backgroundColor: euiTheme.colors.lightestShade,
|
||||
}}
|
||||
>
|
||||
<EuiScreenReaderOnly>
|
||||
<span>Searching category: </span>
|
||||
</EuiScreenReaderOnly>
|
||||
{selectedCategoryTitle}
|
||||
<button
|
||||
data-test-subj="epmList.categoryBadge.closeBtn"
|
||||
onClick={() => {
|
||||
setCategory('');
|
||||
if (setSelectedSubCategory) setSelectedSubCategory(undefined);
|
||||
setUrlandReplaceHistory({
|
||||
searchString: '',
|
||||
categoryId: '',
|
||||
subCategoryId: '',
|
||||
});
|
||||
}}
|
||||
aria-label="Remove filter"
|
||||
style={{
|
||||
padding: euiTheme.size.xs,
|
||||
paddingTop: '2px',
|
||||
}}
|
||||
>
|
||||
<EuiIcon
|
||||
type="cross"
|
||||
color="text"
|
||||
size="s"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontWeight: euiTheme.font.weight.bold,
|
||||
width: 'auto',
|
||||
padding: 0,
|
||||
backgroundColor: euiTheme.colors.lightestShade,
|
||||
}}
|
||||
>
|
||||
<EuiScreenReaderOnly>
|
||||
<span>Searching category: </span>
|
||||
</EuiScreenReaderOnly>
|
||||
{selectedCategoryTitle}
|
||||
<button
|
||||
data-test-subj="epmList.categoryBadge.closeBtn"
|
||||
onClick={() => {
|
||||
setSelectedCategory('');
|
||||
}}
|
||||
aria-label="Remove filter"
|
||||
style={{
|
||||
padding: euiTheme.size.xs,
|
||||
paddingTop: '2px',
|
||||
}}
|
||||
>
|
||||
<EuiIcon
|
||||
type="cross"
|
||||
color="text"
|
||||
size="s"
|
||||
style={{
|
||||
width: 'auto',
|
||||
padding: 0,
|
||||
backgroundColor: euiTheme.colors.lightestShade,
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
</EuiText>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
{callout ? (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
{callout}
|
||||
</>
|
||||
/>
|
||||
</button>
|
||||
</EuiText>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
{showIntegrationsSubcategories && availableSubCategories?.length ? <EuiSpacer /> : null}
|
||||
{showIntegrationsSubcategories ? (
|
||||
<EuiFlexGroup
|
||||
data-test-subj="epmList.subcategoriesRow"
|
||||
justifyContent="flexStart"
|
||||
direction="row"
|
||||
gutterSize="s"
|
||||
style={{
|
||||
maxWidth: 943,
|
||||
}}
|
||||
>
|
||||
{visibleSubCategories?.map((subCategory) => (
|
||||
<EuiFlexItem grow={false} key={subCategory.id}>
|
||||
<EuiButton
|
||||
color="text"
|
||||
aria-label={subCategory?.title}
|
||||
onClick={() => onSubCategoryClick(subCategory.id)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.epmList.subcategoriesButton"
|
||||
defaultMessage="{subcategory}"
|
||||
values={{
|
||||
subcategory: subCategory.title,
|
||||
}}
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
{hiddenSubCategoriesItems?.length ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
data-test-subj="epmList.showMoreSubCategoriesButton"
|
||||
id="moreSubCategories"
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
display="base"
|
||||
onClick={onButtonClick}
|
||||
iconType="boxesHorizontal"
|
||||
aria-label="Show more subcategories"
|
||||
size="m"
|
||||
/>
|
||||
}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downLeft"
|
||||
>
|
||||
<EuiContextMenuPanel size="s" items={hiddenSubCategoriesItems} />
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
) : null}
|
||||
{callout ? (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
{gridContent}
|
||||
{showMissingIntegrationMessage && (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<MissingIntegrationContent
|
||||
resetQuery={resetQuery}
|
||||
setSelectedCategory={setSelectedCategory}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</>
|
||||
{callout}
|
||||
</>
|
||||
) : null}
|
||||
<EuiSpacer />
|
||||
<GridColumn
|
||||
isLoading={isLoading || !localSearchRef.current}
|
||||
list={filteredPromotedList}
|
||||
showMissingIntegrationMessage={showMissingIntegrationMessage}
|
||||
showCardLabels={showCardLabels}
|
||||
/>
|
||||
{showMissingIntegrationMessage && (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<MissingIntegrationContent
|
||||
setUrlandPushHistory={setUrlandPushHistory}
|
||||
resetQuery={resetQuery}
|
||||
setSelectedCategory={setCategory}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
</>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
interface ControlsColumnProps {
|
||||
controls: ReactNode;
|
||||
title: string | undefined;
|
||||
sticky: boolean;
|
||||
}
|
||||
|
||||
function ControlsColumn({ controls, title, sticky }: ControlsColumnProps) {
|
||||
const ControlsColumn = ({ controls, title }: ControlsColumnProps) => {
|
||||
let titleContent;
|
||||
if (title) {
|
||||
titleContent = (
|
||||
|
@ -235,24 +353,28 @@ function ControlsColumn({ controls, title, sticky }: ControlsColumnProps) {
|
|||
);
|
||||
}
|
||||
return (
|
||||
<EuiFlexGroup direction="column" className={sticky ? 'kbnStickyMenu' : ''} gutterSize="none">
|
||||
<EuiFlexGroup direction="column" gutterSize="none">
|
||||
{titleContent}
|
||||
{controls}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
interface GridColumnProps {
|
||||
list: IntegrationCardItem[];
|
||||
isLoading: boolean;
|
||||
showMissingIntegrationMessage?: boolean;
|
||||
showCardLabels?: boolean;
|
||||
}
|
||||
|
||||
function GridColumn({
|
||||
const GridColumn = ({
|
||||
list,
|
||||
showMissingIntegrationMessage = false,
|
||||
showCardLabels = false,
|
||||
}: GridColumnProps) {
|
||||
isLoading,
|
||||
}: GridColumnProps) => {
|
||||
if (isLoading) return <Loading />;
|
||||
|
||||
return (
|
||||
<EuiFlexGrid gutterSize="l" columns={3}>
|
||||
{list.length ? (
|
||||
|
@ -294,21 +416,27 @@ function GridColumn({
|
|||
)}
|
||||
</EuiFlexGrid>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
interface MissingIntegrationContentProps {
|
||||
resetQuery: () => void;
|
||||
setSelectedCategory: (category: string) => void;
|
||||
setSelectedCategory: (category: ExtendedIntegrationCategory) => void;
|
||||
setUrlandPushHistory: (params: IntegrationsURLParameters) => void;
|
||||
}
|
||||
|
||||
function MissingIntegrationContent({
|
||||
const MissingIntegrationContent = ({
|
||||
resetQuery,
|
||||
setSelectedCategory,
|
||||
}: MissingIntegrationContentProps) {
|
||||
setUrlandPushHistory,
|
||||
}: MissingIntegrationContentProps) => {
|
||||
const handleCustomInputsLinkClick = useCallback(() => {
|
||||
resetQuery();
|
||||
setSelectedCategory('custom');
|
||||
}, [resetQuery, setSelectedCategory]);
|
||||
setUrlandPushHistory({
|
||||
categoryId: 'custom',
|
||||
subCategoryId: '',
|
||||
});
|
||||
}, [resetQuery, setSelectedCategory, setUrlandPushHistory]);
|
||||
|
||||
return (
|
||||
<EuiText size="s" color="subdued">
|
||||
|
@ -338,4 +466,4 @@ function MissingIntegrationContent({
|
|||
</p>
|
||||
</EuiText>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -6,48 +6,26 @@
|
|||
*/
|
||||
|
||||
import type { FunctionComponent } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useLocation, useHistory, useParams } from 'react-router-dom';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiHorizontalRule, EuiFlexItem, EuiCallOut, EuiLink } from '@elastic/eui';
|
||||
|
||||
import type { CustomIntegration } from '@kbn/custom-integrations-plugin/common';
|
||||
import { useStartServices } from '../../../../hooks';
|
||||
|
||||
import {
|
||||
isInputOnlyPolicyTemplate,
|
||||
isIntegrationPolicyTemplate,
|
||||
} from '../../../../../../../common/services';
|
||||
import { useBreadcrumbs } from '../../../../hooks';
|
||||
|
||||
import { useCategories, usePackages, useStartServices } from '../../../../hooks';
|
||||
|
||||
import { pagePathGetters } from '../../../../constants';
|
||||
import {
|
||||
useBreadcrumbs,
|
||||
useGetAppendCustomIntegrations,
|
||||
useGetReplacementCustomIntegrations,
|
||||
useLink,
|
||||
} from '../../../../hooks';
|
||||
import { doesPackageHaveIntegrations } from '../../../../services';
|
||||
import type { PackageList } from '../../../../types';
|
||||
import { PackageListGrid } from '../../components/package_list_grid';
|
||||
|
||||
import type { PackageListItem } from '../../../../types';
|
||||
|
||||
import type { IntegrationCardItem } from '../../../../../../../common/types/models';
|
||||
|
||||
import { useMergeEprPackagesWithReplacements } from '../../../../hooks/use_merge_epr_with_replacements';
|
||||
|
||||
import type { IntegrationPreferenceType } from '../../components/integration_preference';
|
||||
import { IntegrationPreference } from '../../components/integration_preference';
|
||||
|
||||
import { mergeCategoriesAndCount } from './util';
|
||||
import { ALL_CATEGORY, CategoryFacets } from './category_facets';
|
||||
import type { CategoryFacet, ExtendedIntegrationCategory } from './category_facets';
|
||||
import { CategoryFacets } from './category_facets';
|
||||
|
||||
import type { CategoryParams } from '.';
|
||||
import { getParams, categoryExists, mapToCard } from '.';
|
||||
import { categoryExists } from '.';
|
||||
|
||||
import { useAvailablePackages } from './hooks/use_available_packages';
|
||||
|
||||
import type { ExtendedIntegrationCategory } from './category_facets';
|
||||
|
||||
const NoEprCallout: FunctionComponent<{ statusCode?: number }> = ({
|
||||
statusCode,
|
||||
|
@ -119,180 +97,35 @@ function OnPremLink() {
|
|||
);
|
||||
}
|
||||
|
||||
function getAllCategoriesFromIntegrations(pkg: PackageListItem) {
|
||||
if (!doesPackageHaveIntegrations(pkg)) {
|
||||
return pkg.categories;
|
||||
}
|
||||
|
||||
const allCategories = pkg.policy_templates?.reduce((accumulator, policyTemplate) => {
|
||||
if (isInputOnlyPolicyTemplate(policyTemplate)) {
|
||||
// input only policy templates do not have categories
|
||||
return accumulator;
|
||||
}
|
||||
return [...accumulator, ...(policyTemplate.categories || [])];
|
||||
}, pkg.categories || []);
|
||||
|
||||
return _.uniq(allCategories);
|
||||
}
|
||||
|
||||
// Packages can export multiple integrations, aka `policy_templates`
|
||||
// In the case where packages ship >1 `policy_templates`, we flatten out the
|
||||
// list of packages by bringing all integrations to top-level so that
|
||||
// each integration is displayed as its own tile
|
||||
const packageListToIntegrationsList = (packages: PackageList): PackageList => {
|
||||
return packages.reduce((acc: PackageList, pkg) => {
|
||||
const {
|
||||
policy_templates: policyTemplates = [],
|
||||
categories: topCategories = [],
|
||||
...restOfPackage
|
||||
} = pkg;
|
||||
|
||||
const topPackage = {
|
||||
...restOfPackage,
|
||||
categories: getAllCategoriesFromIntegrations(pkg),
|
||||
};
|
||||
|
||||
return [
|
||||
...acc,
|
||||
topPackage,
|
||||
...(doesPackageHaveIntegrations(pkg)
|
||||
? policyTemplates.map((policyTemplate) => {
|
||||
const { name, title, description, icons } = policyTemplate;
|
||||
|
||||
const categories =
|
||||
isIntegrationPolicyTemplate(policyTemplate) && policyTemplate.categories
|
||||
? policyTemplate.categories
|
||||
: [];
|
||||
const allCategories = [...topCategories, ...categories];
|
||||
return {
|
||||
...restOfPackage,
|
||||
id: `${restOfPackage.id}-${name}`,
|
||||
integration: name,
|
||||
title,
|
||||
description,
|
||||
icons: icons || restOfPackage.icons,
|
||||
categories: _.uniq(allCategories),
|
||||
};
|
||||
})
|
||||
: []),
|
||||
];
|
||||
}, []);
|
||||
};
|
||||
|
||||
// TODO: clintandrewhall - this component is hard to test due to the hooks, particularly those that use `http`
|
||||
// or `location` to load data. Ideally, we'll split this into "connected" and "pure" components.
|
||||
export const AvailablePackages: React.FC<{}> = ({}) => {
|
||||
const [preference, setPreference] = useState<IntegrationPreferenceType>('recommended');
|
||||
const [prereleaseIntegrationsEnabled, setPrereleaseIntegrationsEnabled] = React.useState<
|
||||
boolean | undefined
|
||||
>(undefined);
|
||||
|
||||
useBreadcrumbs('integrations_all');
|
||||
|
||||
const { http } = useStartServices();
|
||||
const addBasePath = http.basePath.prepend;
|
||||
|
||||
const { selectedCategory, searchParam } = getParams(
|
||||
useParams<CategoryParams>(),
|
||||
useLocation().search
|
||||
);
|
||||
const [category, setCategory] = useState(selectedCategory);
|
||||
|
||||
const history = useHistory();
|
||||
const { getHref, getAbsolutePath } = useLink();
|
||||
|
||||
function setUrlCategory(categoryId: string) {
|
||||
setCategory(categoryId as ExtendedIntegrationCategory);
|
||||
|
||||
const url = pagePathGetters.integrations_all({
|
||||
category: categoryId,
|
||||
searchTerm: searchParam,
|
||||
})[1];
|
||||
history.push(url);
|
||||
}
|
||||
|
||||
function setUrlSearchTerm(search: string) {
|
||||
// Use .replace so the browser's back button is not tied to single keystroke
|
||||
history.replace(pagePathGetters.integrations_all({ searchTerm: search, category })[1]);
|
||||
}
|
||||
|
||||
const {
|
||||
data: eprPackages,
|
||||
isLoading: isLoadingAllPackages,
|
||||
error: eprPackageLoadingError,
|
||||
} = usePackages(prereleaseIntegrationsEnabled);
|
||||
initialSelectedCategory,
|
||||
selectedCategory,
|
||||
setCategory,
|
||||
allCategories,
|
||||
mainCategories,
|
||||
preference,
|
||||
setPreference,
|
||||
isLoadingCategories,
|
||||
isLoadingAllPackages,
|
||||
isLoadingAppendCustomIntegrations,
|
||||
eprPackageLoadingError,
|
||||
eprCategoryLoadingError,
|
||||
searchTerm,
|
||||
setSearchTerm,
|
||||
setUrlandPushHistory,
|
||||
setUrlandReplaceHistory,
|
||||
filteredCards,
|
||||
setPrereleaseIntegrationsEnabled,
|
||||
availableSubCategories,
|
||||
selectedSubCategory,
|
||||
setSelectedSubCategory,
|
||||
} = useAvailablePackages();
|
||||
|
||||
// Remove Kubernetes package granularity
|
||||
if (eprPackages?.items) {
|
||||
eprPackages.items.forEach(function (element) {
|
||||
if (element.id === 'kubernetes') {
|
||||
element.policy_templates = [];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const eprIntegrationList = useMemo(
|
||||
() => packageListToIntegrationsList(eprPackages?.items || []),
|
||||
[eprPackages]
|
||||
);
|
||||
const { value: replacementCustomIntegrations } = useGetReplacementCustomIntegrations();
|
||||
|
||||
const { loading: isLoadingAppendCustomIntegrations, value: appendCustomIntegrations } =
|
||||
useGetAppendCustomIntegrations();
|
||||
|
||||
const mergedEprPackages: Array<PackageListItem | CustomIntegration> =
|
||||
useMergeEprPackagesWithReplacements(
|
||||
preference === 'beats' ? [] : eprIntegrationList,
|
||||
preference === 'agent' ? [] : replacementCustomIntegrations || []
|
||||
);
|
||||
const cards: IntegrationCardItem[] = useMemo(() => {
|
||||
const eprAndCustomPackages = [...mergedEprPackages, ...(appendCustomIntegrations || [])];
|
||||
|
||||
return eprAndCustomPackages
|
||||
.map((item) => {
|
||||
return mapToCard({ getAbsolutePath, getHref, item, addBasePath });
|
||||
})
|
||||
.sort((a, b) => a.title.localeCompare(b.title));
|
||||
}, [addBasePath, appendCustomIntegrations, getAbsolutePath, getHref, mergedEprPackages]);
|
||||
|
||||
const filteredCards = useMemo(
|
||||
() =>
|
||||
cards.filter((c) => {
|
||||
if (category === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return c.categories.includes(category);
|
||||
}),
|
||||
[cards, category]
|
||||
);
|
||||
|
||||
const {
|
||||
data: eprCategories,
|
||||
isLoading: isLoadingCategories,
|
||||
error: eprCategoryLoadingError,
|
||||
} = useCategories(prereleaseIntegrationsEnabled);
|
||||
|
||||
const categories: CategoryFacet[] = useMemo(() => {
|
||||
const eprAndCustomCategories: CategoryFacet[] = isLoadingCategories
|
||||
? []
|
||||
: mergeCategoriesAndCount(
|
||||
eprCategories
|
||||
? (eprCategories.items as Array<{ id: string; title: string; count: number }>)
|
||||
: [],
|
||||
cards
|
||||
);
|
||||
return [
|
||||
{
|
||||
...ALL_CATEGORY,
|
||||
count: cards.length,
|
||||
},
|
||||
...(eprAndCustomCategories ? eprAndCustomCategories : []),
|
||||
];
|
||||
}, [cards, eprCategories, isLoadingCategories]);
|
||||
|
||||
if (!isLoadingCategories && !categoryExists(selectedCategory, categories)) {
|
||||
history.replace(pagePathGetters.integrations_all({ category: '', searchTerm: searchParam })[1]);
|
||||
if (!isLoadingCategories && !categoryExists(initialSelectedCategory, allCategories)) {
|
||||
setUrlandReplaceHistory({ searchString: searchTerm, categoryId: '', subCategoryId: '' });
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -309,17 +142,20 @@ export const AvailablePackages: React.FC<{}> = ({}) => {
|
|||
</EuiFlexItem>,
|
||||
];
|
||||
|
||||
if (categories) {
|
||||
if (mainCategories) {
|
||||
controls = [
|
||||
<EuiFlexItem className="eui-yScrollWithShadows">
|
||||
<CategoryFacets
|
||||
isLoading={
|
||||
isLoadingCategories || isLoadingAllPackages || isLoadingAppendCustomIntegrations
|
||||
}
|
||||
categories={categories}
|
||||
selectedCategory={category}
|
||||
categories={mainCategories}
|
||||
selectedCategory={selectedCategory}
|
||||
onCategoryChange={({ id }) => {
|
||||
setUrlCategory(id);
|
||||
setCategory(id as ExtendedIntegrationCategory);
|
||||
setSearchTerm('');
|
||||
setSelectedSubCategory(undefined);
|
||||
setUrlandPushHistory({ searchString: '', categoryId: id, subCategoryId: '' });
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>,
|
||||
|
@ -337,15 +173,20 @@ export const AvailablePackages: React.FC<{}> = ({}) => {
|
|||
<PackageListGrid
|
||||
isLoading={isLoadingAllPackages || isLoadingAppendCustomIntegrations}
|
||||
controls={controls}
|
||||
initialSearch={searchParam}
|
||||
searchTerm={searchTerm}
|
||||
setSearchTerm={setSearchTerm}
|
||||
list={filteredCards}
|
||||
selectedCategory={category}
|
||||
setSelectedCategory={setUrlCategory}
|
||||
categories={categories}
|
||||
onSearchChange={setUrlSearchTerm}
|
||||
showMissingIntegrationMessage
|
||||
selectedCategory={selectedCategory}
|
||||
setCategory={setCategory}
|
||||
categories={mainCategories}
|
||||
setUrlandReplaceHistory={setUrlandReplaceHistory}
|
||||
setUrlandPushHistory={setUrlandPushHistory}
|
||||
callout={noEprCallout}
|
||||
showCardLabels={false}
|
||||
availableSubCategories={availableSubCategories}
|
||||
selectedSubCategory={selectedSubCategory}
|
||||
setSelectedSubCategory={setSelectedSubCategory}
|
||||
showMissingIntegrationMessage
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -17,6 +17,8 @@ export interface CategoryFacet {
|
|||
count: number;
|
||||
id: string;
|
||||
title: string;
|
||||
parent_id?: string;
|
||||
parent_title?: string;
|
||||
}
|
||||
|
||||
export const UPDATES_AVAILABLE = 'updates_available';
|
||||
|
|
|
@ -0,0 +1,282 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { useLocation, useParams, useHistory } from 'react-router-dom';
|
||||
|
||||
import { uniq, xorBy } from 'lodash';
|
||||
|
||||
import type { CustomIntegration } from '@kbn/custom-integrations-plugin/common';
|
||||
|
||||
import type { IntegrationPreferenceType } from '../../../components/integration_preference';
|
||||
import { usePackages, useCategories, useStartServices } from '../../../../../hooks';
|
||||
import {
|
||||
useGetAppendCustomIntegrations,
|
||||
useGetReplacementCustomIntegrations,
|
||||
useLink,
|
||||
} from '../../../../../hooks';
|
||||
import { useMergeEprPackagesWithReplacements } from '../../../../../hooks/use_merge_epr_with_replacements';
|
||||
|
||||
import type { CategoryParams } from '..';
|
||||
import { getParams, mapToCard } from '..';
|
||||
import type { PackageList, PackageListItem } from '../../../../../types';
|
||||
|
||||
import { doesPackageHaveIntegrations } from '../../../../../services';
|
||||
|
||||
import {
|
||||
isInputOnlyPolicyTemplate,
|
||||
isIntegrationPolicyTemplate,
|
||||
} from '../../../../../../../../common/services';
|
||||
|
||||
import { pagePathGetters } from '../../../../../constants';
|
||||
|
||||
import type { IntegrationCardItem } from '../../../../../../../../common/types/models';
|
||||
|
||||
import { ALL_CATEGORY } from '../category_facets';
|
||||
import type { CategoryFacet } from '../category_facets';
|
||||
|
||||
import { mergeCategoriesAndCount } from '../util';
|
||||
|
||||
export interface IntegrationsURLParameters {
|
||||
searchString?: string;
|
||||
categoryId?: string;
|
||||
subCategoryId?: string;
|
||||
}
|
||||
|
||||
function getAllCategoriesFromIntegrations(pkg: PackageListItem) {
|
||||
if (!doesPackageHaveIntegrations(pkg)) {
|
||||
return pkg.categories;
|
||||
}
|
||||
|
||||
const allCategories = pkg.policy_templates?.reduce((accumulator, policyTemplate) => {
|
||||
if (isInputOnlyPolicyTemplate(policyTemplate)) {
|
||||
// input only policy templates do not have categories
|
||||
return accumulator;
|
||||
}
|
||||
return [...accumulator, ...(policyTemplate.categories || [])];
|
||||
}, pkg.categories || []);
|
||||
|
||||
return uniq(allCategories);
|
||||
}
|
||||
|
||||
// Packages can export multiple integrations, aka `policy_templates`
|
||||
// In the case where packages ship >1 `policy_templates`, we flatten out the
|
||||
// list of packages by bringing all integrations to top-level so that
|
||||
// each integration is displayed as its own tile
|
||||
const packageListToIntegrationsList = (packages: PackageList): PackageList => {
|
||||
return packages.reduce((acc: PackageList, pkg) => {
|
||||
const {
|
||||
policy_templates: policyTemplates = [],
|
||||
categories: topCategories = [],
|
||||
...restOfPackage
|
||||
} = pkg;
|
||||
|
||||
const topPackage = {
|
||||
...restOfPackage,
|
||||
categories: getAllCategoriesFromIntegrations(pkg),
|
||||
};
|
||||
|
||||
return [
|
||||
...acc,
|
||||
topPackage,
|
||||
...(doesPackageHaveIntegrations(pkg)
|
||||
? policyTemplates.map((policyTemplate) => {
|
||||
const { name, title, description, icons } = policyTemplate;
|
||||
|
||||
const categories =
|
||||
isIntegrationPolicyTemplate(policyTemplate) && policyTemplate.categories
|
||||
? policyTemplate.categories
|
||||
: [];
|
||||
const allCategories = [...topCategories, ...categories];
|
||||
return {
|
||||
...restOfPackage,
|
||||
id: `${restOfPackage.id}-${name}`,
|
||||
integration: name,
|
||||
title,
|
||||
description,
|
||||
icons: icons || restOfPackage.icons,
|
||||
categories: uniq(allCategories),
|
||||
};
|
||||
})
|
||||
: []),
|
||||
];
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const useAvailablePackages = () => {
|
||||
const [preference, setPreference] = useState<IntegrationPreferenceType>('recommended');
|
||||
const [prereleaseIntegrationsEnabled, setPrereleaseIntegrationsEnabled] = React.useState<
|
||||
boolean | undefined
|
||||
>(undefined);
|
||||
const { http } = useStartServices();
|
||||
const addBasePath = http.basePath.prepend;
|
||||
|
||||
const {
|
||||
selectedCategory: initialSelectedCategory,
|
||||
selectedSubcategory: initialSubcategory,
|
||||
searchParam,
|
||||
} = getParams(useParams<CategoryParams>(), useLocation().search);
|
||||
|
||||
const [selectedCategory, setCategory] = useState(initialSelectedCategory);
|
||||
const [selectedSubCategory, setSelectedSubCategory] = useState<string | undefined>(
|
||||
initialSubcategory
|
||||
);
|
||||
const [searchTerm, setSearchTerm] = useState(searchParam || '');
|
||||
|
||||
const { getHref, getAbsolutePath } = useLink();
|
||||
const history = useHistory();
|
||||
|
||||
const buildUrl = ({ searchString, categoryId, subCategoryId }: IntegrationsURLParameters) => {
|
||||
const url = pagePathGetters.integrations_all({
|
||||
category: categoryId ? categoryId : '',
|
||||
subCategory: subCategoryId ? subCategoryId : '',
|
||||
searchTerm: searchString ? searchString : '',
|
||||
})[1];
|
||||
return url;
|
||||
};
|
||||
|
||||
const setUrlandPushHistory = ({
|
||||
searchString,
|
||||
categoryId,
|
||||
subCategoryId,
|
||||
}: IntegrationsURLParameters) => {
|
||||
const url = buildUrl({
|
||||
categoryId,
|
||||
searchString,
|
||||
subCategoryId,
|
||||
});
|
||||
history.push(url);
|
||||
};
|
||||
|
||||
const setUrlandReplaceHistory = ({
|
||||
searchString,
|
||||
categoryId,
|
||||
subCategoryId,
|
||||
}: IntegrationsURLParameters) => {
|
||||
const url = buildUrl({
|
||||
categoryId,
|
||||
searchString,
|
||||
subCategoryId,
|
||||
});
|
||||
// Use .replace so the browser's back button is not tied to single keystroke
|
||||
history.replace(url);
|
||||
};
|
||||
|
||||
const {
|
||||
data: eprPackages,
|
||||
isLoading: isLoadingAllPackages,
|
||||
error: eprPackageLoadingError,
|
||||
} = usePackages(prereleaseIntegrationsEnabled);
|
||||
|
||||
// Remove Kubernetes package granularity
|
||||
if (eprPackages?.items) {
|
||||
eprPackages.items.forEach(function (element) {
|
||||
if (element.id === 'kubernetes') {
|
||||
element.policy_templates = [];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const eprIntegrationList = useMemo(
|
||||
() => packageListToIntegrationsList(eprPackages?.items || []),
|
||||
[eprPackages]
|
||||
);
|
||||
const { value: replacementCustomIntegrations } = useGetReplacementCustomIntegrations();
|
||||
|
||||
const { loading: isLoadingAppendCustomIntegrations, value: appendCustomIntegrations } =
|
||||
useGetAppendCustomIntegrations();
|
||||
|
||||
const mergedEprPackages: Array<PackageListItem | CustomIntegration> =
|
||||
useMergeEprPackagesWithReplacements(
|
||||
preference === 'beats' ? [] : eprIntegrationList,
|
||||
preference === 'agent' ? [] : replacementCustomIntegrations || []
|
||||
);
|
||||
|
||||
const cards: IntegrationCardItem[] = useMemo(() => {
|
||||
const eprAndCustomPackages = [...mergedEprPackages, ...(appendCustomIntegrations || [])];
|
||||
|
||||
return eprAndCustomPackages
|
||||
.map((item) => {
|
||||
return mapToCard({ getAbsolutePath, getHref, item, addBasePath });
|
||||
})
|
||||
.sort((a, b) => a.title.localeCompare(b.title));
|
||||
}, [addBasePath, appendCustomIntegrations, getAbsolutePath, getHref, mergedEprPackages]);
|
||||
|
||||
// Packages to show
|
||||
// Filters out based on selected category and subcategory (if any)
|
||||
const filteredCards = useMemo(
|
||||
() =>
|
||||
cards.filter((c) => {
|
||||
if (selectedCategory === '') {
|
||||
return true;
|
||||
}
|
||||
if (!selectedSubCategory) return c.categories.includes(selectedCategory);
|
||||
|
||||
return c.categories.includes(selectedSubCategory);
|
||||
}),
|
||||
[cards, selectedCategory, selectedSubCategory]
|
||||
);
|
||||
|
||||
const {
|
||||
data: eprCategories,
|
||||
isLoading: isLoadingCategories,
|
||||
error: eprCategoryLoadingError,
|
||||
} = useCategories(prereleaseIntegrationsEnabled);
|
||||
|
||||
// Subcategories
|
||||
const subCategories = useMemo(() => {
|
||||
return eprCategories?.items.filter((item) => item.parent_id !== undefined);
|
||||
}, [eprCategories?.items]);
|
||||
|
||||
const allCategories: CategoryFacet[] = useMemo(() => {
|
||||
const eprAndCustomCategories: CategoryFacet[] = isLoadingCategories
|
||||
? []
|
||||
: mergeCategoriesAndCount(
|
||||
eprCategories
|
||||
? (eprCategories.items as Array<{ id: string; title: string; count: number }>)
|
||||
: [],
|
||||
cards
|
||||
);
|
||||
return [
|
||||
{
|
||||
...ALL_CATEGORY,
|
||||
count: cards.length,
|
||||
},
|
||||
...(eprAndCustomCategories ? eprAndCustomCategories : []),
|
||||
];
|
||||
}, [cards, eprCategories, isLoadingCategories]);
|
||||
|
||||
// Filter out subcategories
|
||||
const mainCategories = xorBy(allCategories, subCategories, 'id');
|
||||
|
||||
const availableSubCategories = useMemo(() => {
|
||||
return subCategories?.filter((c) => c.parent_id === selectedCategory);
|
||||
}, [selectedCategory, subCategories]);
|
||||
|
||||
return {
|
||||
initialSelectedCategory,
|
||||
selectedCategory,
|
||||
setCategory,
|
||||
allCategories,
|
||||
mainCategories,
|
||||
availableSubCategories,
|
||||
selectedSubCategory,
|
||||
setSelectedSubCategory,
|
||||
searchTerm,
|
||||
setSearchTerm,
|
||||
setUrlandPushHistory,
|
||||
setUrlandReplaceHistory,
|
||||
preference,
|
||||
setPreference,
|
||||
isLoadingCategories,
|
||||
isLoadingAllPackages,
|
||||
isLoadingAppendCustomIntegrations,
|
||||
eprPackageLoadingError,
|
||||
eprCategoryLoadingError,
|
||||
filteredCards,
|
||||
setPrereleaseIntegrationsEnabled,
|
||||
};
|
||||
};
|
|
@ -35,14 +35,15 @@ import { AvailablePackages } from './available_packages';
|
|||
|
||||
export interface CategoryParams {
|
||||
category?: ExtendedIntegrationCategory;
|
||||
subcategory?: string;
|
||||
}
|
||||
|
||||
export const getParams = (params: CategoryParams, search: string) => {
|
||||
const { category } = params;
|
||||
const { category, subcategory } = params;
|
||||
const selectedCategory: ExtendedIntegrationCategory = category || '';
|
||||
const queryParams = new URLSearchParams(search);
|
||||
const searchParam = queryParams.get(INTEGRATIONS_SEARCH_QUERYPARAM) || '';
|
||||
return { selectedCategory, searchParam };
|
||||
return { selectedCategory, searchParam, selectedSubcategory: subcategory };
|
||||
};
|
||||
|
||||
export const categoryExists = (category: string, categories: CategoryFacet[]) => {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useLocation, useHistory, useParams } from 'react-router-dom';
|
||||
import semverLt from 'semver/functions/lt';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -19,7 +19,9 @@ import { PackageListGrid } from '../../components/package_list_grid';
|
|||
|
||||
import type { PackageListItem } from '../../../../types';
|
||||
|
||||
import type { CategoryFacet } from './category_facets';
|
||||
import type { IntegrationsURLParameters } from './hooks/use_available_packages';
|
||||
|
||||
import type { CategoryFacet, ExtendedIntegrationCategory } from './category_facets';
|
||||
import { CategoryFacets } from './category_facets';
|
||||
|
||||
import type { CategoryParams } from '.';
|
||||
|
@ -113,8 +115,6 @@ const VerificationWarningCallout: React.FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
// TODO: clintandrewhall - this component is hard to test due to the hooks, particularly those that use `http`
|
||||
// or `location` to load data. Ideally, we'll split this into "connected" and "pure" components.
|
||||
export const InstalledPackages: React.FC<{
|
||||
installedPackages: PackageListItem[];
|
||||
isLoading: boolean;
|
||||
|
@ -125,34 +125,42 @@ export const InstalledPackages: React.FC<{
|
|||
|
||||
const { getHref, getAbsolutePath } = useLink();
|
||||
|
||||
const { selectedCategory, searchParam } = getParams(
|
||||
const { selectedCategory: initialSelectedCategory, searchParam } = getParams(
|
||||
useParams<CategoryParams>(),
|
||||
useLocation().search
|
||||
);
|
||||
const [selectedCategory, setCategory] = useState(initialSelectedCategory);
|
||||
const [searchTerm, setSearchTerm] = useState(searchParam || '');
|
||||
|
||||
const { http } = useStartServices();
|
||||
const addBasePath = http.basePath.prepend;
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
function setUrlCategory(categoryId: string) {
|
||||
const buildUrl = ({ searchString, categoryId, subCategoryId }: IntegrationsURLParameters) => {
|
||||
const url = pagePathGetters.integrations_installed({
|
||||
category: categoryId,
|
||||
searchTerm: searchParam,
|
||||
category: categoryId ? categoryId : '',
|
||||
query: searchString ? searchString : '',
|
||||
})[1];
|
||||
return url;
|
||||
};
|
||||
|
||||
const setUrlandPushHistory = ({ searchString, categoryId }: IntegrationsURLParameters) => {
|
||||
const url = buildUrl({
|
||||
categoryId,
|
||||
searchString,
|
||||
});
|
||||
history.push(url);
|
||||
}
|
||||
};
|
||||
|
||||
function setUrlSearchTerm(search: string) {
|
||||
const setUrlandReplaceHistory = ({ searchString, categoryId }: IntegrationsURLParameters) => {
|
||||
const url = buildUrl({
|
||||
categoryId,
|
||||
searchString,
|
||||
});
|
||||
// Use .replace so the browser's back button is not tied to single keystroke
|
||||
history.replace(
|
||||
pagePathGetters.integrations_installed({
|
||||
searchTerm: search,
|
||||
selectedCategory,
|
||||
})[1]
|
||||
);
|
||||
}
|
||||
history.replace(url);
|
||||
};
|
||||
|
||||
const updatablePackages = useMemo(
|
||||
() =>
|
||||
|
@ -178,10 +186,7 @@ export const InstalledPackages: React.FC<{
|
|||
);
|
||||
|
||||
if (!categoryExists(selectedCategory, categories)) {
|
||||
history.replace(
|
||||
pagePathGetters.integrations_installed({ category: '', searchTerm: searchParam })[1]
|
||||
);
|
||||
|
||||
setUrlandReplaceHistory({ searchString: searchTerm, categoryId: '' });
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -189,7 +194,11 @@ export const InstalledPackages: React.FC<{
|
|||
<CategoryFacets
|
||||
categories={categories}
|
||||
selectedCategory={selectedCategory}
|
||||
onCategoryChange={({ id }: CategoryFacet) => setUrlCategory(id)}
|
||||
onCategoryChange={({ id }: CategoryFacet) => {
|
||||
setCategory(id as ExtendedIntegrationCategory);
|
||||
setSearchTerm('');
|
||||
setUrlandPushHistory({ searchString: '', categoryId: id });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -221,9 +230,11 @@ export const InstalledPackages: React.FC<{
|
|||
<PackageListGrid
|
||||
{...{ isLoading, controls, callout, categories }}
|
||||
selectedCategory={selectedCategory}
|
||||
setSelectedCategory={setUrlCategory}
|
||||
onSearchChange={setUrlSearchTerm}
|
||||
initialSearch={searchParam}
|
||||
setCategory={setCategory}
|
||||
setUrlandPushHistory={setUrlandPushHistory}
|
||||
setUrlandReplaceHistory={setUrlandReplaceHistory}
|
||||
searchTerm={searchTerm}
|
||||
setSearchTerm={setSearchTerm}
|
||||
list={cards}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -91,7 +91,7 @@ export const FLEET_ROUTING_PATHS = {
|
|||
export const INTEGRATIONS_SEARCH_QUERYPARAM = 'q';
|
||||
export const INTEGRATIONS_ROUTING_PATHS = {
|
||||
integrations: '/:tabId',
|
||||
integrations_all: '/browse/:category?',
|
||||
integrations_all: '/browse/:category?/:subcategory?',
|
||||
integrations_installed: '/installed/:category?',
|
||||
integrations_installed_updates_available: '/installed/updates_available/:category?',
|
||||
integration_details: '/detail/:pkgkey/:panel?',
|
||||
|
@ -114,8 +114,21 @@ export const pagePathGetters: {
|
|||
base: () => [FLEET_BASE_PATH, '/'],
|
||||
overview: () => [FLEET_BASE_PATH, '/'],
|
||||
integrations: () => [INTEGRATIONS_BASE_PATH, '/'],
|
||||
integrations_all: ({ searchTerm, category }: { searchTerm?: string; category?: string }) => {
|
||||
const categoryPath = category ? `/${category}` : ``;
|
||||
integrations_all: ({
|
||||
searchTerm,
|
||||
category,
|
||||
subCategory,
|
||||
}: {
|
||||
searchTerm?: string;
|
||||
category?: string;
|
||||
subCategory?: string;
|
||||
}) => {
|
||||
const categoryPath =
|
||||
category && subCategory
|
||||
? `/${category}/${subCategory} `
|
||||
: category && !subCategory
|
||||
? `/${category}`
|
||||
: ``;
|
||||
const queryParams = searchTerm ? `?${INTEGRATIONS_SEARCH_QUERYPARAM}=${searchTerm}` : ``;
|
||||
return [INTEGRATIONS_BASE_PATH, `/browse${categoryPath}${queryParams}`];
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue