[Fleet] Add a visual indication of selected subcategory in Integrations page (#149954)

Closes https://github.com/elastic/kibana/issues/149306

## Summary
Display a clear indication of selected subcategory in Integrations page


https://user-images.githubusercontent.com/16084106/215807007-63dbea8d-4496-497f-b4f4-673825a21049.mov

To test it locally, enable feature flag `showIntegrationsSubcategories`.

Some screenshots:

<img width="2040" alt="Screenshot 2023-01-31 at 16 12 35"
src="https://user-images.githubusercontent.com/16084106/215807361-382eb4fa-736c-4073-bf44-79d1d9a3109c.png">

<img width="1563" alt="Screenshot 2023-01-31 at 16 36 38"
src="https://user-images.githubusercontent.com/16084106/215807406-f7d52c44-d1d1-4f4a-b32a-26122ab8cfbe.png">

<img width="1507" alt="Screenshot 2023-01-31 at 16 36 51"
src="https://user-images.githubusercontent.com/16084106/215807430-00189482-2dd3-418c-99b9-0651b82305b7.png">

I also split some of the components in `packageList` since that file is
becoming too big and extracted another hook from `useAvailablePackages`,
this hook only deals with the URL and the history.

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Cristina Amico 2023-02-01 16:36:07 +01:00 committed by GitHub
parent 612b8e7d8a
commit c8c27d7def
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 667 additions and 522 deletions

View file

@ -1,469 +0,0 @@
/*
* 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 type { ReactNode, FunctionComponent } from 'react';
import { useMemo } from 'react';
import React, { useCallback, useState } from 'react';
import { css } from '@emotion/react';
import {
EuiFlexGrid,
EuiFlexGroup,
EuiFlexItem,
EuiLink,
EuiSpacer,
EuiTitle,
EuiFieldSearch,
EuiText,
useEuiTheme,
EuiIcon,
EuiScreenReaderOnly,
EuiButton,
EuiButtonIcon,
EuiPopover,
EuiContextMenuPanel,
EuiContextMenuItem,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { Loading } from '../../../components';
import { useLocalSearch, searchIdField } from '../../../hooks';
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';
export interface Props {
isLoading?: boolean;
controls?: ReactNode | ReactNode[];
list: IntegrationCardItem[];
searchTerm: string;
setSearchTerm: (search: string) => void;
selectedCategory: ExtendedIntegrationCategory;
setCategory: (category: ExtendedIntegrationCategory) => void;
categories: CategoryFacet[];
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> = ({
isLoading,
controls,
title,
list,
searchTerm,
setSearchTerm,
selectedCategory,
setCategory,
categories,
availableSubCategories,
setSelectedSubCategory,
selectedSubCategory,
setUrlandReplaceHistory,
setUrlandPushHistory,
showMissingIntegrationMessage = false,
callout,
showCardLabels = true,
}) => {
const localSearchRef = useLocalSearch(list);
const { euiTheme } = useEuiTheme();
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);
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;
const filteredPromotedList = useMemo(() => {
if (isLoading) return [];
const filteredList = searchTerm
? list.filter((item) =>
(localSearchRef.current!.search(searchTerm) as IntegrationCardItem[])
.map((match) => match[searchIdField])
.includes(item[searchIdField])
)
: list;
return promoteFeaturedIntegrations(filteredList, selectedCategory);
}, [isLoading, list, localSearchRef, searchTerm, selectedCategory]);
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 {};
};
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 (
<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={{
width: 'auto',
padding: 0,
backgroundColor: euiTheme.colors.lightestShade,
}}
/>
</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 />
{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;
}
const ControlsColumn = ({ controls, title }: ControlsColumnProps) => {
let titleContent;
if (title) {
titleContent = (
<>
<EuiTitle size="s">
<h2>{title}</h2>
</EuiTitle>
<EuiSpacer size="l" />
</>
);
}
return (
<EuiFlexGroup direction="column" gutterSize="none">
{titleContent}
{controls}
</EuiFlexGroup>
);
};
interface GridColumnProps {
list: IntegrationCardItem[];
isLoading: boolean;
showMissingIntegrationMessage?: boolean;
showCardLabels?: boolean;
}
const GridColumn = ({
list,
showMissingIntegrationMessage = false,
showCardLabels = false,
isLoading,
}: GridColumnProps) => {
if (isLoading) return <Loading />;
return (
<EuiFlexGrid gutterSize="l" columns={3}>
{list.length ? (
list.map((item) => {
return (
<EuiFlexItem
key={item.id}
// Ensure that cards wrapped in EuiTours/EuiPopovers correctly inherit the full grid row height
css={css`
& > .euiPopover,
& > .euiPopover > .euiPopover__anchor,
& > .euiPopover > .euiPopover__anchor > .euiCard {
height: 100%;
}
`}
>
<PackageCard {...item} showLabels={showCardLabels} />
</EuiFlexItem>
);
})
) : (
<EuiFlexItem grow={3}>
<EuiText>
<p>
{showMissingIntegrationMessage ? (
<FormattedMessage
id="xpack.fleet.epmList.missingIntegrationPlaceholder"
defaultMessage="We didn't find any integrations matching your search term. Please try another keyword or browse using the categories on the left."
/>
) : (
<FormattedMessage
id="xpack.fleet.epmList.noPackagesFoundPlaceholder"
defaultMessage="No integrations found"
/>
)}
</p>
</EuiText>
</EuiFlexItem>
)}
</EuiFlexGrid>
);
};
interface MissingIntegrationContentProps {
resetQuery: () => void;
setSelectedCategory: (category: ExtendedIntegrationCategory) => void;
setUrlandPushHistory: (params: IntegrationsURLParameters) => void;
}
const MissingIntegrationContent = ({
resetQuery,
setSelectedCategory,
setUrlandPushHistory,
}: MissingIntegrationContentProps) => {
const handleCustomInputsLinkClick = useCallback(() => {
resetQuery();
setSelectedCategory('custom');
setUrlandPushHistory({
categoryId: 'custom',
subCategoryId: '',
});
}, [resetQuery, setSelectedCategory, setUrlandPushHistory]);
return (
<EuiText size="s" color="subdued">
<p>
<FormattedMessage
id="xpack.fleet.integrations.missing"
defaultMessage="Don't see an integration? Collect any logs or metrics using our {customInputsLink}. Request new integrations in our {forumLink}."
values={{
customInputsLink: (
<EuiLink onClick={handleCustomInputsLinkClick}>
<FormattedMessage
id="xpack.fleet.integrations.customInputsLink"
defaultMessage="custom inputs"
/>
</EuiLink>
),
forumLink: (
<EuiLink href="https://discuss.elastic.co/tag/integrations" external target="_blank">
<FormattedMessage
id="xpack.fleet.integrations.discussForumLink"
defaultMessage="forum"
/>
</EuiLink>
),
}}
/>
</p>
</EuiText>
);
};

View file

@ -0,0 +1,165 @@
/*
* 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 type { ReactNode } from 'react';
import React, { useCallback } from 'react';
import { css } from '@emotion/react';
import {
EuiFlexGrid,
EuiFlexGroup,
EuiFlexItem,
EuiLink,
EuiSpacer,
EuiTitle,
EuiText,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { Loading } from '../../../../components';
import type { IntegrationCardItem } from '../../../../../../../common/types/models';
import type { ExtendedIntegrationCategory } from '../../screens/home/category_facets';
import type { IntegrationsURLParameters } from '../../screens/home/hooks/use_available_packages';
import { PackageCard } from '../package_card';
interface ControlsColumnProps {
controls: ReactNode;
title: string | undefined;
}
export const ControlsColumn = ({ controls, title }: ControlsColumnProps) => {
let titleContent;
if (title) {
titleContent = (
<>
<EuiTitle size="s">
<h2>{title}</h2>
</EuiTitle>
<EuiSpacer size="l" />
</>
);
}
return (
<EuiFlexGroup direction="column" gutterSize="none">
{titleContent}
{controls}
</EuiFlexGroup>
);
};
interface GridColumnProps {
list: IntegrationCardItem[];
isLoading: boolean;
showMissingIntegrationMessage?: boolean;
showCardLabels?: boolean;
}
export const GridColumn = ({
list,
showMissingIntegrationMessage = false,
showCardLabels = false,
isLoading,
}: GridColumnProps) => {
if (isLoading) return <Loading />;
return (
<EuiFlexGrid gutterSize="l" columns={3}>
{list.length ? (
list.map((item) => {
return (
<EuiFlexItem
key={item.id}
// Ensure that cards wrapped in EuiTours/EuiPopovers correctly inherit the full grid row height
css={css`
& > .euiPopover,
& > .euiPopover > .euiPopover__anchor,
& > .euiPopover > .euiPopover__anchor > .euiCard {
height: 100%;
}
`}
>
<PackageCard {...item} showLabels={showCardLabels} />
</EuiFlexItem>
);
})
) : (
<EuiFlexItem grow={3}>
<EuiText>
<p>
{showMissingIntegrationMessage ? (
<FormattedMessage
id="xpack.fleet.epmList.missingIntegrationPlaceholder"
defaultMessage="We didn't find any integrations matching your search term. Please try another keyword or browse using the categories on the left."
/>
) : (
<FormattedMessage
id="xpack.fleet.epmList.noPackagesFoundPlaceholder"
defaultMessage="No integrations found"
/>
)}
</p>
</EuiText>
</EuiFlexItem>
)}
</EuiFlexGrid>
);
};
interface MissingIntegrationContentProps {
resetQuery: () => void;
setSelectedCategory: (category: ExtendedIntegrationCategory) => void;
setUrlandPushHistory: (params: IntegrationsURLParameters) => void;
}
export const MissingIntegrationContent = ({
resetQuery,
setSelectedCategory,
setUrlandPushHistory,
}: MissingIntegrationContentProps) => {
const handleCustomInputsLinkClick = useCallback(() => {
resetQuery();
setSelectedCategory('custom');
setUrlandPushHistory({
categoryId: 'custom',
subCategoryId: '',
});
}, [resetQuery, setSelectedCategory, setUrlandPushHistory]);
return (
<EuiText size="s" color="subdued">
<p>
<FormattedMessage
id="xpack.fleet.integrations.missing"
defaultMessage="Don't see an integration? Collect any logs or metrics using our {customInputsLink}. Request new integrations in our {forumLink}."
values={{
customInputsLink: (
<EuiLink onClick={handleCustomInputsLinkClick}>
<FormattedMessage
id="xpack.fleet.integrations.customInputsLink"
defaultMessage="custom inputs"
/>
</EuiLink>
),
forumLink: (
<EuiLink href="https://discuss.elastic.co/tag/integrations" external target="_blank">
<FormattedMessage
id="xpack.fleet.integrations.discussForumLink"
defaultMessage="forum"
/>
</EuiLink>
),
}}
/>
</p>
</EuiText>
);
};

View file

@ -9,8 +9,8 @@ import React from 'react';
import { action } from '@storybook/addon-actions';
import type { Props } from './package_list_grid';
import { PackageListGrid } from './package_list_grid';
import type { Props } from '.';
import { PackageListGrid } from '.';
export default {
component: PackageListGrid,

View file

@ -0,0 +1,272 @@
/*
* 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 type { ReactNode, FunctionComponent } from 'react';
import { useMemo } from 'react';
import React, { useCallback, useState } from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiButton,
EuiButtonIcon,
EuiPopover,
EuiContextMenuPanel,
EuiContextMenuItem,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { useLocalSearch, searchIdField } from '../../../../hooks';
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 { ControlsColumn, MissingIntegrationContent, GridColumn } from './controls';
import { SearchBox } from './search_box';
export interface Props {
isLoading?: boolean;
controls?: ReactNode | ReactNode[];
list: IntegrationCardItem[];
searchTerm: string;
setSearchTerm: (search: string) => void;
selectedCategory: ExtendedIntegrationCategory;
setCategory: (category: ExtendedIntegrationCategory) => void;
categories: CategoryFacet[];
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> = ({
isLoading,
controls,
title,
list,
searchTerm,
setSearchTerm,
selectedCategory,
setCategory,
categories,
availableSubCategories,
setSelectedSubCategory,
selectedSubCategory,
setUrlandReplaceHistory,
setUrlandPushHistory,
showMissingIntegrationMessage = false,
callout,
showCardLabels = true,
}) => {
const localSearchRef = useLocalSearch(list);
const [isPopoverOpen, setPopover] = useState(false);
const MAX_SUBCATEGORIES_NUMBER = 6;
const { showIntegrationsSubcategories } = ExperimentalFeaturesService.get();
const onButtonClick = () => {
setPopover(!isPopoverOpen);
};
const closePopover = () => {
setPopover(false);
};
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 filteredPromotedList = useMemo(() => {
if (isLoading) return [];
const filteredList = searchTerm
? list.filter((item) =>
(localSearchRef.current!.search(searchTerm) as IntegrationCardItem[])
.map((match) => match[searchIdField])
.includes(item[searchIdField])
)
: list;
return promoteFeaturedIntegrations(filteredList, selectedCategory);
}, [isLoading, list, localSearchRef, searchTerm, selectedCategory]);
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 {};
};
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();
}}
icon={selectedSubCategory === subCategory.id ? 'check' : 'empty'}
>
{subCategory.title}
</EuiContextMenuItem>
);
});
}, [onSubCategoryClick, selectedSubCategory, splitSubcat?.hiddenSubCategories]);
return (
<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">
<SearchBox
searchTerm={searchTerm}
setSearchTerm={setSearchTerm}
selectedCategory={selectedCategory}
setCategory={setCategory}
categories={categories}
availableSubCategories={availableSubCategories}
setSelectedSubCategory={setSelectedSubCategory}
selectedSubCategory={selectedSubCategory}
setUrlandReplaceHistory={setUrlandReplaceHistory}
/>
{showIntegrationsSubcategories && availableSubCategories?.length ? <EuiSpacer /> : null}
{showIntegrationsSubcategories ? (
<EuiFlexGroup
data-test-subj="epmList.subcategoriesRow"
justifyContent="flexStart"
direction="row"
gutterSize="s"
style={{
maxWidth: 943,
}}
>
{visibleSubCategories?.map((subCategory) => {
const isSelected = subCategory.id === selectedSubCategory;
return (
<EuiFlexItem grow={false} key={subCategory.id}>
<EuiButton
css={isSelected ? 'color: white' : ''}
color={isSelected ? 'accent' : 'text'}
fill={isSelected}
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 />
{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>
);
};

View file

@ -0,0 +1,134 @@
/*
* 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 type { FunctionComponent } from 'react';
import React, { useMemo } from 'react';
import { EuiFieldSearch, EuiText, useEuiTheme, EuiIcon, EuiScreenReaderOnly } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import type {
ExtendedIntegrationCategory,
CategoryFacet,
} from '../../screens/home/category_facets';
import type { IntegrationsURLParameters } from '../../screens/home/hooks/use_available_packages';
export interface Props {
searchTerm: string;
setSearchTerm: (search: string) => void;
selectedCategory: ExtendedIntegrationCategory;
setCategory: (category: ExtendedIntegrationCategory) => void;
categories: CategoryFacet[];
availableSubCategories?: CategoryFacet[];
setUrlandReplaceHistory: (params: IntegrationsURLParameters) => void;
selectedSubCategory?: string;
setSelectedSubCategory?: (c: string | undefined) => void;
}
export const SearchBox: FunctionComponent<Props> = ({
searchTerm,
setSearchTerm,
selectedCategory,
setCategory,
categories,
availableSubCategories,
setSelectedSubCategory,
selectedSubCategory,
setUrlandReplaceHistory,
}) => {
const { euiTheme } = useEuiTheme();
const onQueryChange = (e: any) => {
const queryText = e.target.value;
setSearchTerm(queryText);
setUrlandReplaceHistory({
searchString: queryText,
categoryId: selectedCategory,
subCategoryId: selectedSubCategory,
});
};
const selectedCategoryTitle = selectedCategory
? categories.find((category) => category.id === selectedCategory)?.title
: undefined;
const getCategoriesLabel = useMemo(() => {
const selectedSubCategoryTitle =
selectedSubCategory && availableSubCategories
? availableSubCategories.find((subCat) => subCat.id === selectedSubCategory)?.title
: undefined;
if (selectedCategoryTitle && selectedSubCategoryTitle) {
return `${selectedCategoryTitle}, ${selectedSubCategoryTitle}`;
} else if (selectedCategoryTitle) {
return `${selectedCategoryTitle}`;
} else return '';
}, [availableSubCategories, selectedCategoryTitle, selectedSubCategory]);
return (
<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>
{getCategoriesLabel}
<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={{
width: 'auto',
padding: 0,
backgroundColor: euiTheme.colors.lightestShade,
}}
/>
</button>
</EuiText>
) : undefined
}
/>
);
};

View file

@ -5,23 +5,20 @@
* 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 { usePackages, useCategories } 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 { mapToCard } from '..';
import type { PackageList, PackageListItem } from '../../../../../types';
import { doesPackageHaveIntegrations } from '../../../../../services';
@ -31,8 +28,6 @@ import {
isIntegrationPolicyTemplate,
} from '../../../../../../../../common/services';
import { pagePathGetters } from '../../../../../constants';
import type { IntegrationCardItem } from '../../../../../../../../common/types/models';
import { ALL_CATEGORY } from '../category_facets';
@ -40,6 +35,8 @@ import type { CategoryFacet } from '../category_facets';
import { mergeCategoriesAndCount } from '../util';
import { useBuildIntegrationsUrl } from './use_build_integrations_url';
export interface IntegrationsURLParameters {
searchString?: string;
categoryId?: string;
@ -111,14 +108,17 @@ export const useAvailablePackages = () => {
const [prereleaseIntegrationsEnabled, setPrereleaseIntegrationsEnabled] = React.useState<
boolean | undefined
>(undefined);
const { http } = useStartServices();
const addBasePath = http.basePath.prepend;
const {
selectedCategory: initialSelectedCategory,
selectedSubcategory: initialSubcategory,
initialSelectedCategory,
initialSubcategory,
setUrlandPushHistory,
setUrlandReplaceHistory,
getHref,
getAbsolutePath,
searchParam,
} = getParams(useParams<CategoryParams>(), useLocation().search);
addBasePath,
} = useBuildIntegrationsUrl();
const [selectedCategory, setCategory] = useState(initialSelectedCategory);
const [selectedSubCategory, setSelectedSubCategory] = useState<string | undefined>(
@ -126,45 +126,6 @@ export const useAvailablePackages = () => {
);
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,

View file

@ -0,0 +1,82 @@
/*
* 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 { useLocation, useParams, useHistory } from 'react-router-dom';
import { useStartServices } from '../../../../../hooks';
import { useLink } from '../../../../../hooks';
import type { CategoryParams } from '..';
import { getParams } from '..';
import { pagePathGetters } from '../../../../../constants';
export interface IntegrationsURLParameters {
searchString?: string;
categoryId?: string;
subCategoryId?: string;
}
export const useBuildIntegrationsUrl = () => {
const { http } = useStartServices();
const addBasePath = http.basePath.prepend;
const {
selectedCategory: initialSelectedCategory,
selectedSubcategory: initialSubcategory,
searchParam,
} = getParams(useParams<CategoryParams>(), useLocation().search);
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);
};
return {
initialSelectedCategory,
initialSubcategory,
setUrlandPushHistory,
setUrlandReplaceHistory,
getHref,
getAbsolutePath,
searchParam,
addBasePath,
};
};