[fleet] Adjust unified integration view to have better UI controls (#114692)

* [fleet] Adjust Package Cards to horizontal layout

* Fix responsive shifting

* Addressing feedback

* cleanup layout for integrations view

* i18n

* Fix type errors

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Dave Snider <dave.snider@gmail.com>
Co-authored-by: Josh Dover <1813008+joshdover@users.noreply.github.com>
This commit is contained in:
Clint Andrew Hall 2021-10-14 09:18:01 -05:00 committed by GitHub
parent 56a2e788ca
commit 864e6f1a74
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 132 additions and 161 deletions

View file

@ -42,6 +42,12 @@
#app-fixed-viewport {
top: $headerHeight;
}
.kbnStickyMenu {
position: sticky;
max-height: calc(100vh - #{$headerHeight + $euiSize});
top: $headerHeight + $euiSize;
}
}
.kbnBody {

View file

@ -5,20 +5,12 @@
* 2.0.
*/
import React, { memo } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiImage, EuiSpacer, EuiText, EuiLink } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import styled, { useTheme } from 'styled-components';
import type { EuiTheme } from 'src/plugins/kibana_react/common';
import { useLink } from '../../../hooks';
import type { Section } from '../sections';
import { useLinks, useStartServices } from '../hooks';
import { WithHeaderLayout } from './';
interface Props {
@ -26,45 +18,11 @@ interface Props {
children?: React.ReactNode;
}
const Illustration = styled(EuiImage)`
margin-bottom: -77px;
position: relative;
top: -16px;
width: 395px;
`;
const Hero = styled.div`
text-align: right;
`;
const HeroImage = memo(() => {
const { toSharedAssets } = useLinks();
const theme = useTheme() as EuiTheme;
const IS_DARK_THEME = theme.darkMode;
return (
<Hero>
<Illustration
alt={i18n.translate('xpack.fleet.epm.illustrationAltText', {
defaultMessage: 'Illustration of an integration',
})}
url={
IS_DARK_THEME
? toSharedAssets('illustration_integrations_darkmode.svg')
: toSharedAssets('illustration_integrations_lightmode.svg')
}
/>
</Hero>
);
});
export const DefaultLayout: React.FunctionComponent<Props> = memo(({ section, children }) => {
const { getHref } = useLink();
const { docLinks } = useStartServices();
return (
<WithHeaderLayout
rightColumn={<HeroImage />}
leftColumn={
<EuiFlexGroup direction="column" gutterSize="none" justifyContent="center">
<EuiText>
@ -79,20 +37,11 @@ export const DefaultLayout: React.FunctionComponent<Props> = memo(({ section, ch
<EuiSpacer size="s" />
<EuiFlexItem grow={false}>
<EuiText size="m" color="subdued">
<EuiText size="s" color="subdued">
<p>
<FormattedMessage
id="xpack.fleet.epm.pageSubtitle"
defaultMessage="Collect data from popular applications and services. To learn more about Integrations, view {link}"
values={{
link: (
<EuiLink target="_blank" href={docLinks.links.elasticStackGetStarted}>
{i18n.translate('xpack.fleet.epm.pageSubtitleLinkText', {
defaultMessage: 'Getting started with Elastic Stack',
})}
</EuiLink>
),
}}
defaultMessage="Choose an integration to start collecting and analyzing your data"
/>
</p>
</EuiText>

View file

@ -22,7 +22,7 @@ export default {
type Args = Omit<PackageCardProps, 'status'> & { width: number };
const args: Args = {
width: 250,
width: 280,
title: 'Title',
description: 'Description',
name: 'beats',

View file

@ -7,7 +7,7 @@
import React from 'react';
import styled from 'styled-components';
import { EuiCard } from '@elastic/eui';
import { EuiCard, EuiFlexItem, EuiBadge, EuiToolTip, EuiSpacer } from '@elastic/eui';
import { CardIcon } from '../../../../../components/package_icon';
import type { IntegrationCardItem } from '../../../../../../common/types/models/epm';
@ -16,10 +16,10 @@ import { RELEASE_BADGE_DESCRIPTION, RELEASE_BADGE_LABEL } from './release_badge'
export type PackageCardProps = IntegrationCardItem;
// adding the `href` causes EuiCard to use a `a` instead of a `button`
// `a` tags use `euiLinkColor` which results in blueish Badge text
// Min-height is roughly 3 lines of content.
// This keeps the cards from looking overly unbalanced because of content differences.
const Card = styled(EuiCard)`
color: inherit;
min-height: 127px;
`;
export function PackageCard({
@ -32,14 +32,28 @@ export function PackageCard({
url,
release,
}: PackageCardProps) {
const betaBadgeLabel = release && release !== 'ga' ? RELEASE_BADGE_LABEL[release] : undefined;
const betaBadgeLabelTooltipContent =
release && release !== 'ga' ? RELEASE_BADGE_DESCRIPTION[release] : undefined;
let releaseBadge: React.ReactNode | null = null;
if (release && release !== 'ga') {
releaseBadge = (
<EuiFlexItem grow={false}>
<EuiSpacer size="xs" />
<span>
<EuiToolTip display="inlineBlock" content={RELEASE_BADGE_DESCRIPTION[release]}>
<EuiBadge color="hollow">{RELEASE_BADGE_LABEL[release]}</EuiBadge>
</EuiToolTip>
</span>
</EuiFlexItem>
);
}
return (
<Card
layout="horizontal"
title={title || ''}
titleSize="xs"
description={description}
hasBorder
icon={
<CardIcon
icons={icons}
@ -50,9 +64,9 @@ export function PackageCard({
/>
}
href={url}
betaBadgeLabel={betaBadgeLabel}
betaBadgeTooltipContent={betaBadgeLabelTooltipContent}
target={url.startsWith('http') || url.startsWith('https') ? '_blank' : undefined}
/>
>
{releaseBadge}
</Card>
);
}

View file

@ -5,8 +5,8 @@
* 2.0.
*/
import type { ReactNode } from 'react';
import React, { Fragment, useCallback, useState } from 'react';
import type { ReactNode, FunctionComponent } from 'react';
import React, { useCallback, useState, useRef, useEffect } from 'react';
import {
EuiFlexGrid,
EuiFlexGroup,
@ -20,7 +20,6 @@ import {
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { useStartServices } from '../../../../../hooks';
import { Loading } from '../../../components';
import { useLocalSearch, searchIdField } from '../../../hooks';
@ -31,7 +30,7 @@ import { PackageCard } from './package_card';
export interface Props {
isLoading?: boolean;
controls?: ReactNode | ReactNode[];
title: string;
title?: string;
list: IntegrationCardItem[];
initialSearch?: string;
setSelectedCategory: (category: string) => void;
@ -40,7 +39,7 @@ export interface Props {
callout?: JSX.Element | null;
}
export function PackageListGrid({
export const PackageListGrid: FunctionComponent<Props> = ({
isLoading,
controls,
title,
@ -50,9 +49,23 @@ export function PackageListGrid({
setSelectedCategory,
showMissingIntegrationMessage = false,
callout,
}: Props) {
}) => {
const [searchTerm, setSearchTerm] = useState(initialSearch || '');
const localSearchRef = useLocalSearch(list);
const menuRef = useRef<HTMLDivElement>(null);
const [isSticky, setIsSticky] = useState(false);
const [windowScrollY] = useState(window.scrollY);
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 onQueryChange = ({
queryText: userInput,
@ -71,7 +84,7 @@ export function PackageListGrid({
setSearchTerm('');
};
const controlsContent = <ControlsColumn title={title} controls={controls} />;
const controlsContent = <ControlsColumn title={title} controls={controls} sticky={isSticky} />;
let gridContent: JSX.Element;
if (isLoading || !localSearchRef.current) {
@ -93,58 +106,68 @@ export function PackageListGrid({
}
return (
<EuiFlexGroup alignItems="flexStart">
<EuiFlexItem grow={1}>{controlsContent}</EuiFlexItem>
<EuiFlexItem grow={3}>
<EuiSearchBar
query={searchTerm || undefined}
box={{
placeholder: i18n.translate('xpack.fleet.epmList.searchPackagesPlaceholder', {
defaultMessage: 'Search for integrations',
}),
incremental: true,
}}
onChange={onQueryChange}
/>
{callout ? (
<>
<EuiSpacer />
{callout}
</>
) : null}
<EuiSpacer />
{gridContent}
{showMissingIntegrationMessage && (
<>
<EuiSpacer size="xxl" />
<MissingIntegrationContent
resetQuery={resetQuery}
setSelectedCategory={setSelectedCategory}
/>
</>
)}
</EuiFlexItem>
</EuiFlexGroup>
<div ref={menuRef}>
<EuiFlexGroup alignItems="flexStart" gutterSize="xl">
<EuiFlexItem grow={1} className={isSticky ? 'kbnStickyMenu' : ''}>
{controlsContent}
</EuiFlexItem>
<EuiFlexItem grow={5}>
<EuiSearchBar
query={searchTerm || undefined}
box={{
placeholder: i18n.translate('xpack.fleet.epmList.searchPackagesPlaceholder', {
defaultMessage: 'Search for integrations',
}),
incremental: true,
}}
onChange={onQueryChange}
/>
{callout ? (
<>
<EuiSpacer />
{callout}
</>
) : null}
<EuiSpacer />
{gridContent}
{showMissingIntegrationMessage && (
<>
<EuiSpacer />
<MissingIntegrationContent
resetQuery={resetQuery}
setSelectedCategory={setSelectedCategory}
/>
</>
)}
</EuiFlexItem>
</EuiFlexGroup>
</div>
);
}
};
interface ControlsColumnProps {
controls: ReactNode;
title: string;
title: string | undefined;
sticky: boolean;
}
function ControlsColumn({ controls, title }: ControlsColumnProps) {
function ControlsColumn({ controls, title, sticky }: ControlsColumnProps) {
let titleContent;
if (title) {
titleContent = (
<>
<EuiTitle size="s">
<h2>{title}</h2>
</EuiTitle>
<EuiSpacer size="l" />
</>
);
}
return (
<Fragment>
<EuiTitle size="s">
<h2>{title}</h2>
</EuiTitle>
<EuiSpacer size="l" />
<EuiFlexGroup>
<EuiFlexItem grow={4}>{controls}</EuiFlexItem>
<EuiFlexItem grow={1} />
</EuiFlexGroup>
</Fragment>
<EuiFlexGroup direction="column" className={sticky ? 'kbnStickyMenu' : ''} gutterSize="none">
{titleContent}
{controls}
</EuiFlexGroup>
);
}
@ -196,20 +219,17 @@ function MissingIntegrationContent({
resetQuery,
setSelectedCategory,
}: MissingIntegrationContentProps) {
const {
application: { getUrlForApp },
} = useStartServices();
const handleCustomInputsLinkClick = useCallback(() => {
resetQuery();
setSelectedCategory('custom');
}, [resetQuery, setSelectedCategory]);
return (
<EuiText>
<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}, or add data using {beatsTutorialLink}. Request new integrations using our {discussForumLink}."
defaultMessage="Don't see an integration? Collect any logs or metrics using our {customInputsLink}. Request new integrations using our {discussForumLink}."
values={{
customInputsLink: (
<EuiLink onClick={handleCustomInputsLinkClick}>
@ -227,14 +247,6 @@ function MissingIntegrationContent({
/>
</EuiLink>
),
beatsTutorialLink: (
<EuiLink href={getUrlForApp('home', { path: '#/tutorial_directory' })}>
<FormattedMessage
id="xpack.fleet.integrations.beatsModulesLink"
defaultMessage="Beats modules"
/>
</EuiLink>
),
}}
/>
</p>

View file

@ -7,9 +7,8 @@
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 { EuiHorizontalRule, EuiFlexItem } from '@elastic/eui';
import { pagePathGetters } from '../../../../constants';
import {
@ -93,10 +92,6 @@ const packageListToIntegrationsList = (packages: PackageList): PackageList => {
}, []);
};
const title = i18n.translate('xpack.fleet.epmList.allTitle', {
defaultMessage: 'Browse by category',
});
// 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(() => {
@ -121,9 +116,7 @@ export const AvailablePackages: React.FC = memo(() => {
function setSearchTerm(search: string) {
// Use .replace so the browser's back button is not tied to single keystroke
history.replace(
pagePathGetters.integrations_all({ category: selectedCategory, searchTerm: search })[1]
);
history.replace(pagePathGetters.integrations_all({ searchTerm: search })[1]);
}
const { data: eprPackages, isLoading: isLoadingAllPackages } = useGetPackages({
@ -186,20 +179,26 @@ export const AvailablePackages: React.FC = memo(() => {
}
let controls = [
<EuiHorizontalRule />,
<IntegrationPreference initialType={preference} onChange={setPreference} />,
<EuiFlexItem grow={false}>
<EuiHorizontalRule margin="m" />
<IntegrationPreference initialType={preference} onChange={setPreference} />,
</EuiFlexItem>,
];
if (categories) {
controls = [
<CategoryFacets
isLoading={isLoadingCategories || isLoadingAllPackages || isLoadingAppendCustomIntegrations}
categories={categories}
selectedCategory={selectedCategory}
onCategoryChange={({ id }) => {
setSelectedCategory(id);
}}
/>,
<EuiFlexItem className="eui-yScrollWithShadows">
<CategoryFacets
isLoading={
isLoadingCategories || isLoadingAllPackages || isLoadingAppendCustomIntegrations
}
categories={categories}
selectedCategory={selectedCategory}
onCategoryChange={({ id }) => {
setSelectedCategory(id);
}}
/>
</EuiFlexItem>,
...controls,
];
}
@ -214,7 +213,6 @@ export const AvailablePackages: React.FC = memo(() => {
return (
<PackageListGrid
isLoading={isLoadingAllPackages}
title={title}
controls={controls}
initialSearch={searchParam}
list={filteredCards}

View file

@ -21,7 +21,7 @@ export interface CategoryFacet {
export const ALL_CATEGORY = {
id: '',
title: i18n.translate('xpack.fleet.epmList.allPackagesFilterLinkText', {
defaultMessage: 'All',
defaultMessage: 'All categories',
}),
};

View file

@ -23,9 +23,9 @@ export const CardIcon: React.FunctionComponent<UsePackageIconType & Omit<EuiIcon
) => {
const { icons } = props;
if (icons && icons.length === 1 && icons[0].type === 'eui') {
return <EuiIcon size={'xl'} type={icons[0].src} />;
return <EuiIcon size={'xl'} type={icons[0].src} {...props} />;
} else if (icons && icons.length === 1 && icons[0].type === 'svg') {
return <EuiIcon size={'xl'} type={icons[0].src} />;
return <EuiIcon size={'xl'} type={icons[0].src} {...props} />;
} else {
return <PackageIcon {...props} />;
}

View file

@ -10720,7 +10720,6 @@
"xpack.fleet.epm.detailsTitle": "詳細",
"xpack.fleet.epm.errorLoadingNotice": "NOTICE.txtの読み込みエラー",
"xpack.fleet.epm.featuresLabel": "機能",
"xpack.fleet.epm.illustrationAltText": "統合の例",
"xpack.fleet.epm.install.packageInstallError": "{pkgName} {pkgVersion}のインストールエラー",
"xpack.fleet.epm.install.packageUpdateError": "{pkgName} {pkgVersion}の更新エラー",
"xpack.fleet.epm.licenseLabel": "ライセンス",
@ -10755,7 +10754,6 @@
"xpack.fleet.epm.usedByLabel": "エージェントポリシー",
"xpack.fleet.epm.versionLabel": "バージョン",
"xpack.fleet.epmList.allPackagesFilterLinkText": "すべて",
"xpack.fleet.epmList.allTitle": "カテゴリで参照",
"xpack.fleet.epmList.installedTitle": "インストールされている統合",
"xpack.fleet.epmList.missingIntegrationPlaceholder": "検索用語と一致する統合が見つかりませんでした。別のキーワードを試すか、左側のカテゴリを使用して参照してください。",
"xpack.fleet.epmList.noPackagesFoundPlaceholder": "パッケージが見つかりません",
@ -10829,12 +10827,10 @@
"xpack.fleet.homeIntegration.tutorialModule.noticeText.notePrefix": "注:",
"xpack.fleet.hostsInput.addRow": "行の追加",
"xpack.fleet.initializationErrorMessageTitle": "Fleet を初期化できません",
"xpack.fleet.integrations.beatsModulesLink": "Beatsモジュール",
"xpack.fleet.integrations.customInputsLink": "カスタム入力",
"xpack.fleet.integrations.discussForumLink": "ディスカッションフォーラム",
"xpack.fleet.integrations.installPackage.installingPackageButtonLabel": "{title} アセットをインストールしています",
"xpack.fleet.integrations.installPackage.installPackageButtonLabel": "{title}アセットをインストール",
"xpack.fleet.integrations.missing": "統合が表示されない場合{customInputsLink}を使用してログまたはメトリックを収集するか、{beatsTutorialLink}を使用してデータを追加してください。{discussForumLink}を使用して新しい統合を要求してください。",
"xpack.fleet.integrations.packageInstallErrorDescription": "このパッケージのインストール中に問題が発生しました。しばらくたってから再試行してください。",
"xpack.fleet.integrations.packageInstallErrorTitle": "{title}パッケージをインストールできませんでした",
"xpack.fleet.integrations.packageInstallSuccessDescription": "正常に{title}をインストールしました",

View file

@ -10834,7 +10834,6 @@
"xpack.fleet.epm.detailsTitle": "详情",
"xpack.fleet.epm.errorLoadingNotice": "加载 NOTICE.txt 时出错",
"xpack.fleet.epm.featuresLabel": "功能",
"xpack.fleet.epm.illustrationAltText": "集成的图示",
"xpack.fleet.epm.install.packageInstallError": "安装 {pkgName} {pkgVersion} 时出错",
"xpack.fleet.epm.install.packageUpdateError": "将 {pkgName} 更新到 {pkgVersion} 时出错",
"xpack.fleet.epm.licenseLabel": "许可证",
@ -10869,7 +10868,6 @@
"xpack.fleet.epm.usedByLabel": "代理策略",
"xpack.fleet.epm.versionLabel": "版本",
"xpack.fleet.epmList.allPackagesFilterLinkText": "全部",
"xpack.fleet.epmList.allTitle": "按类别浏览",
"xpack.fleet.epmList.installedTitle": "已安装集成",
"xpack.fleet.epmList.missingIntegrationPlaceholder": "我们未找到任何匹配搜索词的集成。请重试其他关键字,或使用左侧的类别浏览。",
"xpack.fleet.epmList.noPackagesFoundPlaceholder": "未找到任何软件包",
@ -10943,12 +10941,10 @@
"xpack.fleet.homeIntegration.tutorialModule.noticeText.notePrefix": "注意:",
"xpack.fleet.hostsInput.addRow": "添加行",
"xpack.fleet.initializationErrorMessageTitle": "无法初始化 Fleet",
"xpack.fleet.integrations.beatsModulesLink": "Beats 模板",
"xpack.fleet.integrations.customInputsLink": "定制输入",
"xpack.fleet.integrations.discussForumLink": "讨论论坛",
"xpack.fleet.integrations.installPackage.installingPackageButtonLabel": "正在安装 {title} 资产",
"xpack.fleet.integrations.installPackage.installPackageButtonLabel": "安装 {title} 资产",
"xpack.fleet.integrations.missing": "未看到集成?使用我们的{customInputsLink}收集任何日志或指标或使用 {beatsTutorialLink} 添加数据。使用{discussForumLink}请求新的集成。",
"xpack.fleet.integrations.packageInstallErrorDescription": "尝试安装此软件包时出现问题。请稍后重试。",
"xpack.fleet.integrations.packageInstallErrorTitle": "无法安装 {title} 软件包",
"xpack.fleet.integrations.packageInstallSuccessDescription": "已成功安装 {title}",