Skip to content

Commit

Permalink
[fleet] Add Integration Preference selector (#114432) (#114910)
Browse files Browse the repository at this point in the history
Co-authored-by: Clint Andrew Hall <[email protected]>
  • Loading branch information
thomasneirynck and clintandrewhall authored Oct 13, 2021
1 parent dd68f7e commit 0686bac
Show file tree
Hide file tree
Showing 12 changed files with 518 additions and 6,309 deletions.
1,861 changes: 20 additions & 1,841 deletions src/plugins/custom_integrations/public/services/stub/fixtures/integrations.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const searchIdField = 'id';
export const fieldsToSearch = ['name', 'title', 'description'];

export function useLocalSearch(packageList: IntegrationCardItem[]) {
const localSearchRef = useRef<LocalSearch | null>(null);
const localSearchRef = useRef<LocalSearch>(new LocalSearch(searchIdField));

useEffect(() => {
const localSearch = new LocalSearch(searchIdField);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* 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 from 'react';
import type { Meta } from '@storybook/react';

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

import { IntegrationPreference as Component } from './integration_preference';

export default {
title: 'Sections/EPM/Integration Preference',
description: '',
decorators: [
(storyFn, { globals }) => (
<div
style={{
padding: 40,
backgroundColor:
globals.euiTheme === 'v8.dark' || globals.euiTheme === 'v7.dark' ? '#1D1E24' : '#FFF',
width: 280,
}}
>
{storyFn()}
</div>
),
],
} as Meta;

export const IntegrationPreference = () => {
return <Component initialType="recommended" onChange={action('onChange')} />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* 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 from 'react';
import styled from 'styled-components';

import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiPanel,
EuiLink,
EuiText,
EuiForm,
EuiRadioGroup,
EuiSpacer,
EuiIconTip,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';

export type IntegrationPreferenceType = 'recommended' | 'beats' | 'agent';

interface Option {
type: IntegrationPreferenceType;
label: React.ReactNode;
}

export interface Props {
initialType: IntegrationPreferenceType;
onChange: (type: IntegrationPreferenceType) => void;
}

const link = (
<EuiLink href="#">
<FormattedMessage
id="xpack.fleet.epm.integrationPreference.titleLink"
defaultMessage="Elastic Agent and Beats"
/>
</EuiLink>
);

const title = (
<FormattedMessage
id="xpack.fleet.epm.integrationPreference.title"
defaultMessage="When an integration is available for {link}, show:"
values={{ link }}
/>
);

const recommendedTooltip = (
<FormattedMessage
id="xpack.fleet.epm.integrationPreference.recommendedTooltip"
defaultMessage="Generally available (GA) integrations are recommended over beta and experimental."
/>
);

const Item = styled(EuiFlexItem)`
padding-left: ${(props) => props.theme.eui.euiSizeXS};
`;

const options: Option[] = [
{
type: 'recommended',
label: (
<EuiFlexGroup alignItems="center" gutterSize="none">
<EuiFlexItem grow={false}>
{i18n.translate('xpack.fleet.epm.integrationPreference.recommendedLabel', {
defaultMessage: 'Recommended',
})}
</EuiFlexItem>
<Item>
<EuiIconTip content={recommendedTooltip} />
</Item>
</EuiFlexGroup>
),
},
{
type: 'agent',
label: i18n.translate('xpack.fleet.epm.integrationPreference.elasticAgentLabel', {
defaultMessage: 'Elastic Agent only',
}),
},
{
type: 'beats',
label: i18n.translate('xpack.fleet.epm.integrationPreference.beatsLabel', {
defaultMessage: 'Beats only',
}),
},
];

export const IntegrationPreference = ({ initialType, onChange }: Props) => {
const [idSelected, setIdSelected] = React.useState<IntegrationPreferenceType>(initialType);
const radios = options.map((option) => ({
id: option.type,
value: option.type,
label: option.label,
}));

return (
<EuiPanel hasShadow={false} paddingSize="none">
<EuiText size="s">{title}</EuiText>
<EuiSpacer size="m" />
<EuiForm>
<EuiRadioGroup
options={radios}
idSelected={idSelected}
onChange={(id, value) => {
setIdSelected(id as IntegrationPreferenceType);
onChange(value as IntegrationPreferenceType);
}}
name="preference"
/>
</EuiForm>
</EuiPanel>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { PackageCard } from './package_card';

export interface Props {
isLoading?: boolean;
controls?: ReactNode;
controls?: ReactNode | ReactNode[];
title: string;
list: IntegrationCardItem[];
initialSearch?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
* 2.0.
*/

import React, { memo, useMemo } from 'react';
import React, { memo, useMemo, useState } from 'react';
import { useLocation, useHistory, useParams } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import _ from 'lodash';
import { EuiHorizontalRule } from '@elastic/eui';

import { pagePathGetters } from '../../../../constants';
import {
Expand All @@ -31,6 +32,9 @@ import type { IntegrationCardItem } from '../../../../../../../common/types/mode

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 } from './category_facets';
Expand Down Expand Up @@ -96,11 +100,14 @@ const title = i18n.translate('xpack.fleet.epmList.allTitle', {
// 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 = memo(() => {
const [preference, setPreference] = useState<IntegrationPreferenceType>('recommended');
useBreadcrumbs('integrations_all');

const { selectedCategory, searchParam } = getParams(
useParams<CategoryParams>(),
useLocation().search
);

const history = useHistory();
const { getHref, getAbsolutePath } = useLink();

Expand All @@ -111,6 +118,7 @@ export const AvailablePackages: React.FC = memo(() => {
})[1];
history.push(url);
}

function setSearchTerm(search: string) {
// Use .replace so the browser's back button is not tied to single keystroke
history.replace(
Expand All @@ -121,32 +129,40 @@ export const AvailablePackages: React.FC = memo(() => {
const { data: eprPackages, isLoading: isLoadingAllPackages } = useGetPackages({
category: '',
});

const eprIntegrationList = useMemo(
() => packageListToIntegrationsList(eprPackages?.response || []),
[eprPackages]
);

const { value: replacementCustomIntegrations } = useGetReplacementCustomIntegrations();

const mergedEprPackages: Array<PackageListItem | CustomIntegration> =
useMergeEprPackagesWithReplacements(
eprIntegrationList || [],
replacementCustomIntegrations || []
preference === 'beats' ? [] : eprIntegrationList,
preference === 'agent' ? [] : replacementCustomIntegrations || []
);

const { loading: isLoadingAppendCustomIntegrations, value: appendCustomIntegrations } =
useGetAppendCustomIntegrations();

const eprAndCustomPackages: Array<CustomIntegration | PackageListItem> = [
...mergedEprPackages,
...(appendCustomIntegrations || []),
];

const cards: IntegrationCardItem[] = eprAndCustomPackages.map((item) => {
return mapToCard(getAbsolutePath, getHref, item);
});

cards.sort((a, b) => {
return a.title.localeCompare(b.title);
});

const { data: eprCategories, isLoading: isLoadingCategories } = useGetCategories({
include_policy_templates: true,
});

const categories = useMemo(() => {
const eprAndCustomCategories: CategoryFacet[] =
isLoadingCategories || !eprCategories
Expand All @@ -169,16 +185,24 @@ export const AvailablePackages: React.FC = memo(() => {
return null;
}

const controls = categories ? (
<CategoryFacets
isLoading={isLoadingCategories || isLoadingAllPackages || isLoadingAppendCustomIntegrations}
categories={categories}
selectedCategory={selectedCategory}
onCategoryChange={({ id }: CategoryFacet) => {
setSelectedCategory(id);
}}
/>
) : null;
let controls = [
<EuiHorizontalRule />,
<IntegrationPreference initialType={preference} onChange={setPreference} />,
];

if (categories) {
controls = [
<CategoryFacets
isLoading={isLoadingCategories || isLoadingAllPackages || isLoadingAppendCustomIntegrations}
categories={categories}
selectedCategory={selectedCategory}
onCategoryChange={({ id }) => {
setSelectedCategory(id);
}}
/>,
...controls,
];
}

const filteredCards = cards.filter((c) => {
if (selectedCategory === '') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { INTEGRATIONS_ROUTING_PATHS } from '../../../../constants';
import { EPMHomePage as Component } from '.';

export default {
component: Component,
title: 'Sections/EPM/Home',
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { DynamicPage, DynamicPagePathValues, StaticPage } from '../../../..
import { INTEGRATIONS_ROUTING_PATHS, INTEGRATIONS_SEARCH_QUERYPARAM } from '../../../../constants';
import { DefaultLayout } from '../../../../layouts';

import type { IntegrationCategory } from '../../../../../../../../../../src/plugins/custom_integrations/common';
import type { CustomIntegration } from '../../../../../../../../../../src/plugins/custom_integrations/common';

import type { PackageListItem } from '../../../../types';
Expand All @@ -31,7 +32,10 @@ export const getParams = (params: CategoryParams, search: string) => {
const selectedCategory = category || '';
const queryParams = new URLSearchParams(search);
const searchParam = queryParams.get(INTEGRATIONS_SEARCH_QUERYPARAM) || '';
return { selectedCategory, searchParam };
return { selectedCategory, searchParam } as {
selectedCategory: IntegrationCategory & '';
searchParam: string;
};
};

export const categoryExists = (category: string, categories: CategoryFacet[]) => {
Expand Down
Loading

0 comments on commit 0686bac

Please sign in to comment.