[Fleet] Revamp integration detail page (#90887)

* Extract integration detail page changes from POC

* Remove unneccessary link wrappers

* Remove unused import

* Fix method name

* Fix linting
This commit is contained in:
Jen Huang 2021-02-10 12:03:34 -08:00 committed by GitHub
parent f3debcd084
commit c2b41c484b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 900 additions and 758 deletions

View file

@ -37,7 +37,7 @@ export interface HeaderProps {
leftColumn?: JSX.Element;
rightColumn?: JSX.Element;
rightColumnGrow?: EuiFlexItemProps['grow'];
tabs?: EuiTabProps[];
tabs?: Array<Omit<EuiTabProps, 'name'> & { name?: JSX.Element | string }>;
tabsClassName?: string;
'data-test-subj'?: string;
}
@ -73,7 +73,7 @@ export const Header: React.FC<HeaderProps> = ({
<EuiSpacer size="s" />
<Tabs className={tabsClassName}>
{tabs.map((props) => (
<EuiTab {...props} key={props.id}>
<EuiTab {...(props as EuiTabProps)} key={props.id}>
{props.name}
</EuiTab>
))}

View file

@ -43,7 +43,7 @@ const TutorialModuleNotice: TutorialModuleNoticeComponent = memo(({ moduleName }
),
availableAsIntegrationLink: (
<EuiLink
href={getHref('integration_details', {
href={getHref('integration_details_overview', {
pkgkey: pkgKeyFromPackageInfo(pkgInfo),
})}
>

View file

@ -18,7 +18,10 @@ export type StaticPage =
| 'data_streams';
export type DynamicPage =
| 'integration_details'
| 'integration_details_overview'
| 'integration_details_policies'
| 'integration_details_settings'
| 'integration_details_custom'
| 'integration_policy_edit'
| 'policy_details'
| 'add_integration_from_policy'
@ -43,6 +46,10 @@ export const PAGE_ROUTING_PATHS = {
integrations_all: '/integrations',
integrations_installed: '/integrations/installed',
integration_details: '/integrations/detail/:pkgkey/:panel?',
integration_details_overview: '/integrations/detail/:pkgkey/overview',
integration_details_policies: '/integrations/detail/:pkgkey/policies',
integration_details_settings: '/integrations/detail/:pkgkey/settings',
integration_details_custom: '/integrations/detail/:pkgkey/custom',
integration_policy_edit: '/integrations/edit-integration/:packagePolicyId',
policies: '/policies',
policies_list: '/policies',
@ -70,8 +77,10 @@ export const pagePathGetters: {
integrations: () => '/integrations',
integrations_all: () => '/integrations',
integrations_installed: () => '/integrations/installed',
integration_details: ({ pkgkey, panel }) =>
`/integrations/detail/${pkgkey}${panel ? `/${panel}` : ''}`,
integration_details_overview: ({ pkgkey }) => `/integrations/detail/${pkgkey}/overview`,
integration_details_policies: ({ pkgkey }) => `/integrations/detail/${pkgkey}/policies`,
integration_details_settings: ({ pkgkey }) => `/integrations/detail/${pkgkey}/settings`,
integration_details_custom: ({ pkgkey }) => `/integrations/detail/${pkgkey}/custom`,
integration_policy_edit: ({ packagePolicyId }) =>
`/integrations/edit-integration/${packagePolicyId}`,
policies: () => '/policies',

View file

@ -18,7 +18,7 @@ const BASE_BREADCRUMB: ChromeBreadcrumb = {
};
const breadcrumbGetters: {
[key in Page]: (values: DynamicPagePathValues) => ChromeBreadcrumb[];
[key in Page]?: (values: DynamicPagePathValues) => ChromeBreadcrumb[];
} = {
base: () => [BASE_BREADCRUMB],
overview: () => [
@ -65,7 +65,7 @@ const breadcrumbGetters: {
}),
},
],
integration_details: ({ pkgTitle }) => [
integration_details_overview: ({ pkgTitle }) => [
BASE_BREADCRUMB,
{
href: pagePathGetters.integrations(),
@ -84,7 +84,7 @@ const breadcrumbGetters: {
}),
},
{
href: pagePathGetters.integration_details({ pkgkey, panel: 'policies' }),
href: pagePathGetters.integration_details_policies({ pkgkey }),
text: pkgTitle,
},
{ text: policyName },
@ -142,7 +142,7 @@ const breadcrumbGetters: {
}),
},
{
href: pagePathGetters.integration_details({ pkgkey }),
href: pagePathGetters.integration_details_overview({ pkgkey }),
text: pkgTitle,
},
{
@ -221,10 +221,11 @@ const breadcrumbGetters: {
export function useBreadcrumbs(page: Page, values: DynamicPagePathValues = {}) {
const { chrome, http } = useStartServices();
const breadcrumbs: ChromeBreadcrumb[] = breadcrumbGetters[page](values).map((breadcrumb) => ({
...breadcrumb,
href: breadcrumb.href ? http.basePath.prepend(`${BASE_PATH}#${breadcrumb.href}`) : undefined,
}));
const breadcrumbs: ChromeBreadcrumb[] =
breadcrumbGetters[page]?.(values).map((breadcrumb) => ({
...breadcrumb,
href: breadcrumb.href ? http.basePath.prepend(`${BASE_PATH}#${breadcrumb.href}`) : undefined,
})) || [];
const docTitle: string[] = [...breadcrumbs]
.reverse()
.map((breadcrumb) => breadcrumb.text as string);

View file

@ -217,7 +217,7 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
}
return from === 'policy'
? getHref('policy_details', { policyId: agentPolicyId || policyId })
: getHref('integration_details', { pkgkey });
: getHref('integration_details_overview', { pkgkey });
}, [agentPolicyId, policyId, from, getHref, pkgkey, routeState]);
const cancelClickHandler: ReactEventHandler = useCallback(

View file

@ -246,9 +246,8 @@ export const EditPackagePolicyForm = memo<{
const cancelUrl = useMemo((): string => {
if (packageInfo && policyId) {
return from === 'package-edit'
? getHref('integration_details', {
? getHref('integration_details_policies', {
pkgkey: pkgKeyFromPackageInfo(packageInfo!),
panel: 'policies',
})
: getHref('policy_details', { policyId });
}
@ -258,9 +257,8 @@ export const EditPackagePolicyForm = memo<{
const successRedirectPath = useMemo(() => {
if (packageInfo && policyId) {
return from === 'package-edit'
? getPath('integration_details', {
? getPath('integration_details_policies', {
pkgkey: pkgKeyFromPackageInfo(packageInfo!),
panel: 'policies',
})
: getPath('policy_details', { policyId });
}

View file

@ -43,7 +43,7 @@ export function PackageCard({
title={title || ''}
description={description}
icon={<PackageIcon icons={icons} packageName={name} version={version} size="xl" />}
href={getHref('integration_details', { pkgkey: `${name}-${urlVersion}` })}
href={getHref('integration_details_overview', { pkgkey: `${name}-${urlVersion}` })}
betaBadgeLabel={release && release !== 'ga' ? RELEASE_BADGE_LABEL[release] : undefined}
betaBadgeTooltipContent={
release && release !== 'ga' ? RELEASE_BADGE_DESCRIPTION[release] : undefined

View file

@ -90,9 +90,8 @@ function usePackageInstall({ notifications }: { notifications: NotificationsStar
} else {
setPackageInstallStatus({ name, status: InstallStatus.installed, version });
if (fromUpdate) {
const settingsPath = getPath('integration_details', {
const settingsPath = getPath('integration_details_settings', {
pkgkey: `${name}-${version}`,
panel: 'settings',
});
history.push(settingsPath);
}

View file

@ -0,0 +1,60 @@
/*
* 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 { EuiIcon, EuiPanel } from '@elastic/eui';
import { usePackageIconType, UsePackageIconType } from '../../../../../hooks';
import { Loading } from '../../../../../components';
const PanelWrapper = styled.div`
// NOTE: changes to the width here will impact navigation tabs page layout under integration package details
width: ${(props) =>
parseFloat(props.theme.eui.euiSize) * 6 + parseFloat(props.theme.eui.euiSizeXL) * 2}px;
height: 1px;
z-index: 1;
`;
const Panel = styled(EuiPanel)`
padding: ${(props) => props.theme.eui.spacerSizes.xl};
margin-bottom: -100%;
svg,
img {
height: ${(props) => parseFloat(props.theme.eui.euiSize) * 6}px;
width: ${(props) => parseFloat(props.theme.eui.euiSize) * 6}px;
}
.euiFlexItem {
height: ${(props) => parseFloat(props.theme.eui.euiSize) * 6}px;
justify-content: center;
}
`;
export function IconPanel({
packageName,
version,
icons,
}: Pick<UsePackageIconType, 'packageName' | 'version' | 'icons'>) {
const iconType = usePackageIconType({ packageName, version, icons });
return (
<PanelWrapper>
<Panel>
<EuiIcon type={iconType} size="original" />
</Panel>
</PanelWrapper>
);
}
export function LoadingIconPanel() {
return (
<PanelWrapper>
<Panel>
<Loading />
</Panel>
</PanelWrapper>
);
}

View file

@ -0,0 +1,9 @@
/*
* 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.
*/
export { UpdateIcon } from './update_icon';
export { IntegrationAgentPolicyCount } from './integration_agent_policy_count';
export { IconPanel, LoadingIconPanel } from './icon_panel';

View file

@ -6,7 +6,7 @@
*/
import React, { memo } from 'react';
import { useGetPackageStats } from '../../../../hooks';
import { useGetPackageStats } from '../../../../../hooks';
/**
* Displays a count of Agent Policies that are using the given integration

View file

@ -0,0 +1,24 @@
/*
* 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 { EuiIconTip, EuiIconProps } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
export const UpdateIcon = ({ size = 'm' }: { size?: EuiIconProps['size'] }) => (
<EuiIconTip
aria-label={i18n.translate('xpack.fleet.epm.updateAvailableTooltip', {
defaultMessage: 'Update available',
})}
size={size}
type="alert"
color="warning"
content={i18n.translate('xpack.fleet.epm.updateAvailableTooltip', {
defaultMessage: 'Update available',
})}
/>
);

View file

@ -1,114 +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 { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import React, { memo, useMemo } from 'react';
import styled from 'styled-components';
import { Redirect } from 'react-router-dom';
import { DetailParams } from '.';
import { DetailViewPanelName, PackageInfo } from '../../../../types';
import { AssetsFacetGroup } from '../../components/assets_facet_group';
import { CenterColumn, LeftColumn, RightColumn } from './layout';
import { OverviewPanel } from './overview_panel';
import { PackagePoliciesPanel } from './package_policies_panel';
import { SettingsPanel } from './settings_panel';
import { useUIExtension } from '../../../../hooks/use_ui_extension';
import { ExtensionWrapper } from '../../../../components/extension_wrapper';
import { useLink } from '../../../../hooks';
import { pkgKeyFromPackageInfo } from '../../../../services/pkg_key_from_package_info';
type ContentProps = PackageInfo & Pick<DetailParams, 'panel'>;
const LeftSideColumn = styled(LeftColumn)`
/* 🤢🤷 https://www.styled-components.com/docs/faqs#how-can-i-override-styles-with-higher-specificity */
&&& {
margin-top: 77px;
}
`;
// fixes IE11 problem with nested flex items
const ContentFlexGroup = styled(EuiFlexGroup)`
flex: 0 0 auto !important;
`;
export function Content(props: ContentProps) {
const { panel } = props;
const showRightColumn = useMemo(() => {
const fullWidthContentPages: DetailViewPanelName[] = ['policies', 'custom'];
return !fullWidthContentPages.includes(panel!);
}, [panel]);
return (
<ContentFlexGroup>
<LeftSideColumn {...(!showRightColumn ? { columnGrow: 1 } : undefined)} />
<CenterColumn {...(!showRightColumn ? { columnGrow: 6 } : undefined)}>
<ContentPanel panel={panel!} packageInfo={props} />
</CenterColumn>
{showRightColumn && (
<RightColumn>
<RightColumnContent {...props} />
</RightColumn>
)}
</ContentFlexGroup>
);
}
interface ContentPanelProps {
packageInfo: PackageInfo;
panel: DetailViewPanelName;
}
export const ContentPanel = memo<ContentPanelProps>(({ panel, packageInfo }) => {
const { name, version, assets, title, removable, latestVersion } = packageInfo;
const pkgkey = pkgKeyFromPackageInfo(packageInfo);
const CustomView = useUIExtension(name, 'package-detail-custom');
const { getPath } = useLink();
switch (panel) {
case 'settings':
return (
<SettingsPanel
name={name}
version={version}
assets={assets}
title={title}
removable={removable}
latestVersion={latestVersion}
/>
);
case 'policies':
return <PackagePoliciesPanel name={name} version={version} />;
case 'custom':
return CustomView ? (
<ExtensionWrapper>
<CustomView pkgkey={pkgkey} packageInfo={packageInfo} />
</ExtensionWrapper>
) : (
<Redirect to={getPath('integration_details', { pkgkey })} />
);
case 'overview':
default:
return <OverviewPanel {...packageInfo} />;
}
});
type RightColumnContentProps = PackageInfo & Pick<DetailParams, 'panel'>;
function RightColumnContent(props: RightColumnContentProps) {
const { assets, panel } = props;
switch (panel) {
case 'overview':
return assets ? (
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem grow={false}>
<AssetsFacetGroup assets={assets} />
</EuiFlexItem>
</EuiFlexGroup>
) : null;
default:
return <EuiSpacer />;
}
}

View file

@ -1,98 +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 { EuiButton, EuiButtonEmpty, EuiHorizontalRule, EuiSpacer } from '@elastic/eui';
import React, { Fragment, useCallback, useLayoutEffect, useRef, useState } from 'react';
import styled from 'styled-components';
const BottomFade = styled.div`
width: 100%;
background: ${(props) =>
`linear-gradient(${props.theme.eui.euiColorEmptyShade}00 0%, ${props.theme.eui.euiColorEmptyShade} 100%)`};
margin-top: -${(props) => parseInt(props.theme.eui.spacerSizes.xl, 10) * 2}px;
height: ${(props) => parseInt(props.theme.eui.spacerSizes.xl, 10) * 2}px;
position: absolute;
`;
const ContentCollapseContainer = styled.div`
position: relative;
`;
const CollapseButtonContainer = styled.div`
display: inline-block;
background-color: ${(props) => props.theme.eui.euiColorEmptyShade};
position: absolute;
left: 50%;
transform: translateX(-50%);
top: ${(props) => parseInt(props.theme.eui.euiButtonHeight, 10) / 2}px;
`;
const CollapseButtonTop = styled(EuiButtonEmpty)`
float: right;
`;
const CollapseButton = ({
open,
toggleCollapse,
}: {
open: boolean;
toggleCollapse: () => void;
}) => {
return (
<div style={{ position: 'relative' }}>
<EuiSpacer size="m" />
<EuiHorizontalRule />
<CollapseButtonContainer>
<EuiButton onClick={toggleCollapse} iconType={`arrow${open ? 'Up' : 'Down'}`}>
{open ? 'Collapse' : 'Read more'}
</EuiButton>
</CollapseButtonContainer>
</div>
);
};
export const ContentCollapse = ({ children }: { children: React.ReactNode }) => {
const [open, setOpen] = useState<boolean>(false);
const [height, setHeight] = useState<number | string>('auto');
const [collapsible, setCollapsible] = useState<boolean>(true);
const contentEl = useRef<HTMLDivElement>(null);
const collapsedHeight = 360;
// if content is too small, don't collapse
useLayoutEffect(
() =>
contentEl.current && contentEl.current.clientHeight < collapsedHeight
? setCollapsible(false)
: setHeight(collapsedHeight),
[]
);
const clickOpen = useCallback(() => {
setOpen(!open);
}, [open]);
return (
<Fragment>
{collapsible ? (
<ContentCollapseContainer>
<div
ref={contentEl}
style={{ height: `${open ? 'auto' : `${height}px`}`, overflow: 'hidden' }}
>
{open && (
<CollapseButtonTop onClick={clickOpen} iconType={`arrow${open ? 'Up' : 'Down'}`}>
Collapse
</CollapseButtonTop>
)}
{children}
</div>
{!open && <BottomFade />}
<CollapseButton open={open} toggleCollapse={clickOpen} />
</ContentCollapseContainer>
) : (
<div>{children}</div>
)}
</Fragment>
);
};

View file

@ -0,0 +1,37 @@
/*
* 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, { memo, useMemo } from 'react';
import { Redirect } from 'react-router-dom';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { useUIExtension } from '../../../../../hooks/use_ui_extension';
import { useLink } from '../../../../../hooks';
import { PackageInfo } from '../../../../../types';
import { pkgKeyFromPackageInfo } from '../../../../../services/pkg_key_from_package_info';
import { ExtensionWrapper } from '../../../../../components/extension_wrapper';
interface Props {
packageInfo: PackageInfo;
}
export const CustomViewPage: React.FC<Props> = memo(({ packageInfo }) => {
const CustomView = useUIExtension(packageInfo.name, 'package-detail-custom');
const { getPath } = useLink();
const pkgkey = useMemo(() => pkgKeyFromPackageInfo(packageInfo), [packageInfo]);
return CustomView ? (
<EuiFlexGroup alignItems="flexStart">
<EuiFlexItem grow={1} />
<EuiFlexItem grow={6}>
<ExtensionWrapper>
<CustomView pkgkey={pkgkey} packageInfo={packageInfo} />
</ExtensionWrapper>
</EuiFlexItem>
</EuiFlexGroup>
) : (
<Redirect to={getPath('integration_details_overview', { pkgkey })} />
);
});

View file

@ -0,0 +1,7 @@
/*
* 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.
*/
export { CustomViewPage } from './custom';

View file

@ -28,7 +28,7 @@ import { act, cleanup } from '@testing-library/react';
describe('when on integration detail', () => {
const pkgkey = 'nginx-0.3.7';
const detailPageUrlPath = pagePathGetters.integration_details({ pkgkey });
const detailPageUrlPath = pagePathGetters.integration_details_overview({ pkgkey });
let testRenderer: TestRenderer;
let renderResult: ReturnType<typeof testRenderer.render>;
let mockedApi: MockedApi<EpmPackageDetailsResponseProvidersMock>;
@ -100,7 +100,7 @@ describe('when on integration detail', () => {
it('should redirect if custom url is accessed', () => {
act(() => {
testRenderer.history.push(
pagePathGetters.integration_details({ pkgkey: 'nginx-0.3.7', panel: 'custom' })
pagePathGetters.integration_details_custom({ pkgkey: 'nginx-0.3.7' })
);
});
expect(testRenderer.history.location.pathname).toEqual(detailPageUrlPath);
@ -148,7 +148,7 @@ describe('when on integration detail', () => {
it('should display custom content when tab is clicked', async () => {
act(() => {
testRenderer.history.push(
pagePathGetters.integration_details({ pkgkey: 'nginx-0.3.7', panel: 'custom' })
pagePathGetters.integration_details_custom({ pkgkey: 'nginx-0.3.7' })
);
});
await lazyComponentWasRendered;
@ -173,14 +173,14 @@ describe('when on integration detail', () => {
onCancelNavigateTo: [
'fleet',
{
path: '#/integrations/detail/nginx-0.3.7',
path: '#/integrations/detail/nginx-0.3.7/overview',
},
],
onCancelUrl: '#/integrations/detail/nginx-0.3.7',
onCancelUrl: '#/integrations/detail/nginx-0.3.7/overview',
onSaveNavigateTo: [
'fleet',
{
path: '#/integrations/detail/nginx-0.3.7',
path: '#/integrations/detail/nginx-0.3.7/overview',
},
],
});
@ -188,7 +188,7 @@ describe('when on integration detail', () => {
});
describe('and on the Policies Tab', () => {
const policiesTabURLPath = pagePathGetters.integration_details({ pkgkey, panel: 'policies' });
const policiesTabURLPath = pagePathGetters.integration_details_policies({ pkgkey });
beforeEach(() => {
testRenderer.history.push(policiesTabURLPath);
render();

View file

@ -4,72 +4,50 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useEffect, useState, useMemo, useCallback, ReactEventHandler } from 'react';
import { useHistory, useLocation, useParams } from 'react-router-dom';
import React, { ReactEventHandler, useCallback, useEffect, useMemo, useState } from 'react';
import { Redirect, Route, Switch, useHistory, useLocation, useParams } from 'react-router-dom';
import styled from 'styled-components';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiButtonEmpty,
EuiText,
EuiSpacer,
EuiBetaBadge,
EuiButton,
EuiButtonEmpty,
EuiDescriptionList,
EuiDescriptionListTitle,
EuiDescriptionListDescription,
EuiDescriptionListTitle,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { useUIExtension } from '../../../../hooks/use_ui_extension';
import { PAGE_ROUTING_PATHS, PLUGIN_ID } from '../../../../constants';
import { useCapabilities, useGetPackageInfoByKey, useLink } from '../../../../hooks';
import { pkgKeyFromPackageInfo } from '../../../../services/pkg_key_from_package_info';
import {
CreatePackagePolicyRouteState,
DetailViewPanelName,
entries,
InstallStatus,
PackageInfo,
} from '../../../../types';
import { Loading, Error } from '../../../../components';
import {
useGetPackageInfoByKey,
useBreadcrumbs,
useLink,
useCapabilities,
} from '../../../../hooks';
import { Error, Loading } from '../../../../components';
import { useBreadcrumbs } from '../../../../hooks';
import { WithHeaderLayout, WithHeaderLayoutProps } from '../../../../layouts';
import { RELEASE_BADGE_DESCRIPTION, RELEASE_BADGE_LABEL } from '../../components/release_badge';
import { useSetPackageInstallStatus } from '../../hooks';
import { IconPanel, LoadingIconPanel } from '../../components/icon_panel';
import { RELEASE_BADGE_LABEL, RELEASE_BADGE_DESCRIPTION } from '../../components/release_badge';
import { UpdateIcon } from '../../components/icons';
import { Content } from './content';
import { IntegrationAgentPolicyCount, UpdateIcon, IconPanel, LoadingIconPanel } from './components';
import { OverviewPage } from './overview';
import { PackagePoliciesPage } from './policies';
import { SettingsPage } from './settings';
import { CustomViewPage } from './custom';
import './index.scss';
import { useUIExtension } from '../../../../hooks/use_ui_extension';
import { PLUGIN_ID } from '../../../../../../../common/constants';
import { pkgKeyFromPackageInfo } from '../../../../services/pkg_key_from_package_info';
import { IntegrationAgentPolicyCount } from './integration_agent_policy_count';
export const DEFAULT_PANEL: DetailViewPanelName = 'overview';
export interface DetailParams {
pkgkey: string;
panel?: DetailViewPanelName;
}
const PanelDisplayNames: Record<DetailViewPanelName, string> = {
overview: i18n.translate('xpack.fleet.epm.packageDetailsNav.overviewLinkText', {
defaultMessage: 'Overview',
}),
policies: i18n.translate('xpack.fleet.epm.packageDetailsNav.packagePoliciesLinkText', {
defaultMessage: 'Policies',
}),
settings: i18n.translate('xpack.fleet.epm.packageDetailsNav.settingsLinkText', {
defaultMessage: 'Settings',
}),
custom: i18n.translate('xpack.fleet.epm.packageDetailsNav.packageCustomLinkText', {
defaultMessage: 'Advanced',
}),
};
const Divider = styled.div`
width: 0;
height: 100%;
@ -82,12 +60,12 @@ const FlexItemWithMinWidth = styled(EuiFlexItem)`
`;
function Breadcrumbs({ packageTitle }: { packageTitle: string }) {
useBreadcrumbs('integration_details', { pkgTitle: packageTitle });
useBreadcrumbs('integration_details_overview', { pkgTitle: packageTitle });
return null;
}
export function Detail() {
const { pkgkey, panel = DEFAULT_PANEL } = useParams<DetailParams>();
const { pkgkey, panel } = useParams<DetailParams>();
const { getHref, getPath } = useLink();
const hasWriteCapabilites = useCapabilities().write;
const history = useHistory();
@ -247,7 +225,7 @@ export function Detail() {
{ isDivider: true },
{
label: i18n.translate('xpack.fleet.epm.usedByLabel', {
defaultMessage: 'Agent Policies',
defaultMessage: 'Agent policies',
}),
'data-test-subj': 'agentPolicyCount',
content: <IntegrationAgentPolicyCount packageName={packageInfo.name} />,
@ -306,37 +284,79 @@ export function Detail() {
]
);
const tabs = useMemo<WithHeaderLayoutProps['tabs']>(() => {
const headerTabs = useMemo<WithHeaderLayoutProps['tabs']>(() => {
if (!packageInfo) {
return [];
}
const packageInfoKey = pkgKeyFromPackageInfo(packageInfo);
return (entries(PanelDisplayNames)
.filter(([panelId]) => {
// Don't show `Policies` tab if package is not installed
if (panelId === 'policies' && packageInstallStatus !== InstallStatus.installed) {
return false;
}
const tabs: WithHeaderLayoutProps['tabs'] = [
{
id: 'overview',
name: (
<FormattedMessage
id="xpack.fleet.epm.packageDetailsNav.overviewLinkText"
defaultMessage="Overview"
/>
),
isSelected: panel === 'overview',
'data-test-subj': `tab-overview`,
href: getHref('integration_details_overview', {
pkgkey: packageInfoKey,
}),
},
];
// Don't show `custom` tab if a custom component is not registered
if (panelId === 'custom' && !showCustomTab) {
return false;
}
if (packageInstallStatus === InstallStatus.installed) {
tabs.push({
id: 'policies',
name: (
<FormattedMessage
id="xpack.fleet.epm.packageDetailsNav.packagePoliciesLinkText"
defaultMessage="Policies"
/>
),
isSelected: panel === 'policies',
'data-test-subj': `tab-policies`,
href: getHref('integration_details_policies', {
pkgkey: packageInfoKey,
}),
});
}
return true;
})
.map(([panelId, display]) => {
return {
id: panelId,
name: display,
isSelected: panelId === panel,
'data-test-subj': `tab-${panelId}`,
href: getHref('integration_details', {
pkgkey: pkgKeyFromPackageInfo(packageInfo || {}),
panel: panelId,
}),
};
}) as unknown) as WithHeaderLayoutProps['tabs'];
tabs.push({
id: 'settings',
name: (
<FormattedMessage
id="xpack.fleet.epm.packageDetailsNav.settingsLinkText"
defaultMessage="Settings"
/>
),
isSelected: panel === 'settings',
'data-test-subj': `tab-settings`,
href: getHref('integration_details_settings', {
pkgkey: packageInfoKey,
}),
});
if (showCustomTab) {
tabs.push({
id: 'custom',
name: (
<FormattedMessage
id="xpack.fleet.epm.packageDetailsNav.packageCustomLinkText"
defaultMessage="Advanced"
/>
),
isSelected: panel === 'custom',
'data-test-subj': `tab-custom`,
href: getHref('integration_details_custom', {
pkgkey: packageInfoKey,
}),
});
}
return tabs;
}, [getHref, packageInfo, panel, showCustomTab, packageInstallStatus]);
return (
@ -344,7 +364,7 @@ export function Detail() {
leftColumn={headerLeftContent}
rightColumn={headerRightContent}
rightColumnGrow={false}
tabs={tabs}
tabs={headerTabs}
tabsClassName="fleet__epm__shiftNavTabs"
>
{packageInfo ? <Breadcrumbs packageTitle={packageInfo.title} /> : null}
@ -361,7 +381,21 @@ export function Detail() {
) : isLoading || !packageInfo ? (
<Loading />
) : (
<Content {...packageInfo} panel={panel} />
<Switch>
<Route path={PAGE_ROUTING_PATHS.integration_details_overview}>
<OverviewPage packageInfo={packageInfo} />
</Route>
<Route path={PAGE_ROUTING_PATHS.integration_details_settings}>
<SettingsPage packageInfo={packageInfo} />
</Route>
<Route path={PAGE_ROUTING_PATHS.integration_details_policies}>
<PackagePoliciesPage name={packageInfo.name} version={packageInfo.version} />
</Route>
<Route path={PAGE_ROUTING_PATHS.integration_details_custom}>
<CustomViewPage packageInfo={packageInfo} />
</Route>
<Redirect to={PAGE_ROUTING_PATHS.integration_details_overview} />
</Switch>
)}
</WithHeaderLayout>
);

View file

@ -1,52 +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 { EuiFlexItem } from '@elastic/eui';
import React, { FunctionComponent, ReactNode } from 'react';
import { FlexItemGrowSize } from '@elastic/eui/src/components/flex/flex_item';
interface ColumnProps {
children?: ReactNode;
className?: string;
columnGrow?: FlexItemGrowSize;
}
export const LeftColumn: FunctionComponent<ColumnProps> = ({
columnGrow = 2,
children,
...rest
}) => {
return (
<EuiFlexItem grow={columnGrow} {...rest}>
{children}
</EuiFlexItem>
);
};
export const CenterColumn: FunctionComponent<ColumnProps> = ({
columnGrow = 9,
children,
...rest
}) => {
return (
<EuiFlexItem grow={columnGrow} {...rest}>
{children}
</EuiFlexItem>
);
};
export const RightColumn: FunctionComponent<ColumnProps> = ({
columnGrow = 3,
children,
...rest
}) => {
return (
<EuiFlexItem grow={columnGrow} {...rest}>
{children}
</EuiFlexItem>
);
};

View file

@ -0,0 +1,159 @@
/*
* 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, { memo, useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiTextColor,
EuiDescriptionList,
EuiNotificationBadge,
} from '@elastic/eui';
import { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list';
import {
PackageInfo,
PackageSpecCategory,
AssetTypeToParts,
KibanaAssetType,
entries,
} from '../../../../../types';
import { useGetCategories } from '../../../../../hooks';
import { AssetTitleMap, DisplayedAssets, ServiceTitleMap } from '../../../constants';
interface Props {
packageInfo: PackageInfo;
}
export const Details: React.FC<Props> = memo(({ packageInfo }) => {
const { data: categoriesData, isLoading: isLoadingCategories } = useGetCategories();
const packageCategories: string[] = useMemo(() => {
if (!isLoadingCategories && categoriesData && categoriesData.response) {
return categoriesData.response
.filter((category) => packageInfo.categories?.includes(category.id as PackageSpecCategory))
.map((category) => category.title);
}
return [];
}, [categoriesData, isLoadingCategories, packageInfo.categories]);
const listItems = useMemo(() => {
// Base details: version and categories
const items: EuiDescriptionListProps['listItems'] = [
{
title: (
<EuiTextColor color="subdued">
<FormattedMessage id="xpack.fleet.epm.versionLabel" defaultMessage="Version" />
</EuiTextColor>
),
description: packageInfo.version,
},
{
title: (
<EuiTextColor color="subdued">
<FormattedMessage id="xpack.fleet.epm.categoryLabel" defaultMessage="Category" />
</EuiTextColor>
),
description: packageCategories.join(', '),
},
];
// Asset details and counts
entries(packageInfo.assets).forEach(([service, typeToParts]) => {
// Filter out assets we are not going to display
// (currently we only display Kibana and Elasticsearch assets)
const filteredTypes: AssetTypeToParts = entries(typeToParts).reduce(
(acc: any, [asset, value]) => {
if (DisplayedAssets[service].includes(asset)) acc[asset] = value;
return acc;
},
{}
);
if (Object.entries(filteredTypes).length) {
items.push({
title: (
<EuiTextColor color="subdued">
<FormattedMessage
id="xpack.fleet.epm.assetGroupTitle"
defaultMessage="{assetType} assets"
values={{
assetType: ServiceTitleMap[service],
}}
/>
</EuiTextColor>
),
description: (
<EuiFlexGroup direction="column" gutterSize="xs">
{entries(filteredTypes).map(([_type, parts]) => {
const type = _type as KibanaAssetType;
return (
<EuiFlexItem>
<EuiFlexGroup gutterSize="xs" alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>{AssetTitleMap[type]}</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiNotificationBadge color="subdued">{parts.length}</EuiNotificationBadge>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
);
})}
</EuiFlexGroup>
),
});
}
});
// Feature (data stream type) details
const dataStreamTypes = [
...new Set(packageInfo.data_streams?.map((dataStream) => dataStream.type) || []),
];
if (dataStreamTypes.length) {
items.push({
title: (
<EuiTextColor color="subdued">
<FormattedMessage id="xpack.fleet.epm.featuresLabel" defaultMessage="Features" />
</EuiTextColor>
),
description: dataStreamTypes.join(', '),
});
}
// License details
if (packageInfo.license) {
items.push({
title: (
<EuiTextColor color="subdued">
<FormattedMessage id="xpack.fleet.epm.licenseLabel" defaultMessage="License" />
</EuiTextColor>
),
description: packageInfo.license,
});
}
return items;
}, [
packageCategories,
packageInfo.assets,
packageInfo.data_streams,
packageInfo.license,
packageInfo.version,
]);
return (
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem>
<EuiText>
<h4>
<FormattedMessage id="xpack.fleet.epm.detailsTitle" defaultMessage="Details" />
</h4>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiDescriptionList type="column" compressed listItems={listItems} />
</EuiFlexItem>
</EuiFlexGroup>
);
});

View file

@ -0,0 +1,7 @@
/*
* 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.
*/
export { OverviewPage } from './overview';

View file

@ -0,0 +1,57 @@
/*
* 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, { memo } from 'react';
import styled from 'styled-components';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { PackageInfo } from '../../../../../types';
import { Screenshots } from './screenshots';
import { Readme } from './readme';
import { Details } from './details';
interface Props {
packageInfo: PackageInfo;
}
const LeftColumn = styled(EuiFlexItem)`
/* 🤢🤷 https://www.styled-components.com/docs/faqs#how-can-i-override-styles-with-higher-specificity */
&&& {
margin-top: 77px;
}
`;
export const OverviewPage: React.FC<Props> = memo(({ packageInfo }: Props) => {
return (
<EuiFlexGroup alignItems="flexStart">
<LeftColumn grow={2} />
<EuiFlexItem grow={9}>
{packageInfo.readme ? (
<Readme
readmePath={packageInfo.readme}
packageName={packageInfo.name}
version={packageInfo.version}
/>
) : null}
</EuiFlexItem>
<EuiFlexItem grow={3}>
<EuiFlexGroup direction="column" gutterSize="l" alignItems="flexStart">
{packageInfo.screenshots && packageInfo.screenshots.length ? (
<EuiFlexItem>
<Screenshots
images={packageInfo.screenshots}
packageName={packageInfo.name}
version={packageInfo.version}
/>
</EuiFlexItem>
) : null}
<EuiFlexItem>
<Details packageInfo={packageInfo} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
);
});

View file

@ -8,10 +8,9 @@
import { EuiLoadingContent, EuiText } from '@elastic/eui';
import React, { Fragment, useEffect, useState } from 'react';
import ReactMarkdown from 'react-markdown';
import { useLinks } from '../../hooks';
import { ContentCollapse } from './content_collapse';
import { useLinks } from '../../../hooks';
import { sendGetFileByPath } from '../../../../../hooks';
import { markdownRenderers } from './markdown_renderers';
import { sendGetFileByPath } from '../../../../hooks';
export function Readme({
readmePath,
@ -43,13 +42,11 @@ export function Readme({
return (
<Fragment>
{markdown !== undefined ? (
<ContentCollapse>
<ReactMarkdown
transformImageUri={handleImageUri}
renderers={markdownRenderers}
source={markdown}
/>
</ContentCollapse>
<ReactMarkdown
transformImageUri={handleImageUri}
renderers={markdownRenderers}
source={markdown}
/>
) : (
<EuiText>
{/* simulates a long page of text loading */}

View file

@ -0,0 +1,94 @@
/*
* 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, memo } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiFlexGroup, EuiFlexItem, EuiImage, EuiText, EuiPagination } from '@elastic/eui';
import { ScreenshotItem } from '../../../../../types';
import { useLinks } from '../../../hooks';
interface ScreenshotProps {
images: ScreenshotItem[];
packageName: string;
version: string;
}
export const Screenshots: React.FC<ScreenshotProps> = memo(({ images, packageName, version }) => {
const { toPackageImage } = useLinks();
const [currentImageIndex, setCurrentImageIndex] = useState<number>(0);
const maxImageIndex = useMemo(() => images.length - 1, [images.length]);
const currentImageUrl = useMemo(
() => toPackageImage(images[currentImageIndex], packageName, version),
[currentImageIndex, images, packageName, toPackageImage, version]
);
return (
<EuiFlexGroup direction="column" gutterSize="s">
{/* Title with carousel navigation */}
<EuiFlexItem>
<EuiFlexGroup
direction="row"
alignItems="center"
gutterSize="xs"
justifyContent="spaceBetween"
>
<EuiFlexItem grow={false}>
<EuiText>
<h4>
<FormattedMessage
id="xpack.fleet.epm.screenshotsTitle"
defaultMessage="Screenshots"
/>
</h4>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPagination
aria-label={i18n.translate('xpack.fleet.epm.screenshotPaginationAriaLabel', {
defaultMessage: '{packageName} screenshot pagination',
values: {
packageName,
},
})}
pageCount={maxImageIndex + 1}
activePage={currentImageIndex}
onPageClick={(activePage) => setCurrentImageIndex(activePage)}
compressed
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
{/* Current screenshot */}
<EuiFlexItem>
{currentImageUrl ? (
<EuiImage
allowFullScreen
hasShadow
alt={
images[currentImageIndex].title ||
i18n.translate('xpack.fleet.epm.screenshotAltText', {
defaultMessage: '{packageName} screenshot #{imageNumber}',
values: {
packageName,
imageNumber: currentImageIndex + 1,
},
})
}
title={images[currentImageIndex].title}
url={currentImageUrl}
/>
) : (
<FormattedMessage
id="xpack.fleet.epm.screenshotErrorText"
defaultMessage="Unable to load this screenshot"
/>
)}
</EuiFlexItem>
</EuiFlexGroup>
);
});

View file

@ -1,23 +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 { EuiSpacer } from '@elastic/eui';
import React, { Fragment } from 'react';
import { PackageInfo } from '../../../../types';
import { Readme } from './readme';
import { Screenshots } from './screenshots';
export function OverviewPanel(props: PackageInfo) {
const { screenshots, readme, name, version } = props;
return (
<Fragment>
{readme && <Readme readmePath={readme} packageName={name} version={version} />}
<EuiSpacer size="xl" />
{screenshots && <Screenshots images={screenshots} packageName={name} version={version} />}
</Fragment>
);
}

View file

@ -0,0 +1,7 @@
/*
* 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.
*/
export { PackagePoliciesPage } from './package_policies';

View file

@ -12,21 +12,22 @@ import {
EuiBasicTable,
EuiLink,
EuiTableFieldDataColumnType,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedRelative, FormattedMessage } from '@kbn/i18n/react';
import { useGetPackageInstallStatus } from '../../hooks';
import { InstallStatus } from '../../../../types';
import { useLink } from '../../../../hooks';
import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../../../common/constants';
import { useUrlPagination } from '../../../../hooks';
import { InstallStatus } from '../../../../../types';
import { useLink, useUrlPagination } from '../../../../../hooks';
import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants';
import { LinkAndRevision, LinkAndRevisionProps } from '../../../../../components';
import { LinkedAgentCount } from '../../../../../components/linked_agent_count';
import { useGetPackageInstallStatus } from '../../../hooks';
import {
PackagePolicyAndAgentPolicy,
usePackagePoliciesWithAgentPolicy,
} from './use_package_policies_with_agent_policy';
import { LinkAndRevision, LinkAndRevisionProps } from '../../../../components';
import { Persona } from './persona';
import { LinkedAgentCount } from '../../../../components/linked_agent_count';
const IntegrationDetailsLink = memo<{
packagePolicy: PackagePolicyAndAgentPolicy['packagePolicy'];
@ -52,6 +53,7 @@ const AgentPolicyDetailLink = memo<{
children: ReactNode;
}>(({ agentPolicyId, revision, children }) => {
const { getHref } = useLink();
return (
<LinkAndRevision
className="eui-textTruncate"
@ -69,7 +71,7 @@ interface PackagePoliciesPanelProps {
name: string;
version: string;
}
export const PackagePoliciesPanel = ({ name, version }: PackagePoliciesPanelProps) => {
export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps) => {
const { getPath } = useLink();
const getPackageInstallStatus = useGetPackageInstallStatus();
const packageInstallStatus = getPackageInstallStatus(name);
@ -197,18 +199,25 @@ export const PackagePoliciesPanel = ({ name, version }: PackagePoliciesPanelProp
// if they arrive at this page and the package is not installed, send them to overview
// this happens if they arrive with a direct url or they uninstall while on this tab
if (packageInstallStatus.status !== InstallStatus.installed) {
return <Redirect to={getPath('integration_details', { pkgkey: `${name}-${version}` })} />;
return (
<Redirect to={getPath('integration_details_overview', { pkgkey: `${name}-${version}` })} />
);
}
return (
<EuiBasicTable
items={data?.items || []}
columns={columns}
loading={isLoading}
data-test-subj="integrationPolicyTable"
pagination={tablePagination}
onChange={handleTableOnChange}
noItemsMessage={noItemsMessage}
/>
<EuiFlexGroup alignItems="flexStart">
<EuiFlexItem grow={1} />
<EuiFlexItem grow={6}>
<EuiBasicTable
items={data?.items || []}
columns={columns}
loading={isLoading}
data-test-subj="integrationPolicyTable"
pagination={tablePagination}
onChange={handleTableOnChange}
noItemsMessage={noItemsMessage}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -4,9 +4,8 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiAvatar, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import React, { CSSProperties, memo, useCallback } from 'react';
import { EuiAvatar, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { EuiAvatarProps } from '@elastic/eui/src/components/avatar/avatar';
const MIN_WIDTH: CSSProperties = { minWidth: 0 };

View file

@ -6,19 +6,19 @@
*/
import { useEffect, useMemo, useState } from 'react';
import { PackagePolicy } from '../../../../../../../common/types/models';
import {
PackagePolicy,
GetAgentPoliciesResponse,
GetAgentPoliciesResponseItem,
} from '../../../../../../../common/types/rest_spec';
import { useGetPackagePolicies } from '../../../../hooks/use_request';
GetPackagePoliciesResponse,
} from '../../../../../types';
import { agentPolicyRouteService } from '../../../../../services';
import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants';
import { useGetPackagePolicies } from '../../../../../hooks';
import {
SendConditionalRequestConfig,
useConditionalRequest,
} from '../../../../hooks/use_request/use_request';
import { agentPolicyRouteService } from '../../../../../../../common/services';
import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../../../../../../../common/constants';
import { GetPackagePoliciesResponse } from '../../../../../../../common/types/rest_spec';
} from '../../../../../hooks/use_request/use_request';
export interface PackagePolicyEnriched extends PackagePolicy {
_agentPolicy: GetAgentPoliciesResponseItem | undefined;

View file

@ -1,92 +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 React, { Fragment } from 'react';
import styled from 'styled-components';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiFlexGroup, EuiFlexItem, EuiImage, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import { ScreenshotItem } from '../../../../types';
import { useLinks } from '../../hooks';
interface ScreenshotProps {
images: ScreenshotItem[];
packageName: string;
version: string;
}
const getHorizontalPadding = (styledProps: any): number =>
parseInt(styledProps.theme.eui.paddingSizes.xl, 10) * 2;
const getVerticalPadding = (styledProps: any): number =>
parseInt(styledProps.theme.eui.paddingSizes.xl, 10) * 1.75;
const getPadding = (styledProps: any) =>
styledProps.hascaption
? `${styledProps.theme.eui.paddingSizes.xl} ${getHorizontalPadding(
styledProps
)}px ${getVerticalPadding(styledProps)}px`
: `${getHorizontalPadding(styledProps)}px ${getVerticalPadding(styledProps)}px`;
const ScreenshotsContainer = styled(EuiFlexGroup)`
background: linear-gradient(360deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0) 100%),
${(styledProps) => styledProps.theme.eui.euiColorPrimary};
padding: ${(styledProps) => getPadding(styledProps)};
flex: 0 0 auto;
border-radius: ${(styledProps) => styledProps.theme.eui.euiBorderRadius};
`;
// fixes ie11 problems with nested flex items
const NestedEuiFlexItem = styled(EuiFlexItem)`
flex: 0 0 auto !important;
`;
export function Screenshots(props: ScreenshotProps) {
const { toPackageImage } = useLinks();
const { images, packageName, version } = props;
// for now, just get first image
const image = images[0];
const hasCaption = image.title ? true : false;
const screenshotUrl = toPackageImage(image, packageName, version);
return (
<Fragment>
<EuiTitle size="s">
<h3>
<FormattedMessage id="xpack.fleet.epm.screenshotsTitle" defaultMessage="Screenshots" />
</h3>
</EuiTitle>
<EuiSpacer size="m" />
<ScreenshotsContainer
gutterSize="none"
direction="column"
alignItems="center"
{...(hasCaption ? { hascaption: 'true' } : {})}
>
{hasCaption && (
<NestedEuiFlexItem>
<EuiText color="ghost" aria-label="screenshot image caption">
{image.title}
</EuiText>
<EuiSpacer />
</NestedEuiFlexItem>
)}
{screenshotUrl && (
<NestedEuiFlexItem>
{/* By default EuiImage sets width to 100% and Figure to 22.5rem for size=l images,
set image to same width. Will need to update if size changes.
*/}
<EuiImage
url={screenshotUrl}
alt="screenshot image preview"
size="l"
allowFullScreen
style={{ width: '22.5rem', maxWidth: '100%' }}
/>
</NestedEuiFlexItem>
)}
</ScreenshotsContainer>
</Fragment>
);
}

View file

@ -0,0 +1,7 @@
/*
* 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.
*/
export { SettingsPage } from './settings';

View file

@ -8,9 +8,9 @@
import { EuiButton } from '@elastic/eui';
import React, { Fragment, useCallback, useMemo, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { PackageInfo, InstallStatus } from '../../../../types';
import { useCapabilities } from '../../../../hooks';
import { useUninstallPackage, useGetPackageInstallStatus, useInstallPackage } from '../../hooks';
import { PackageInfo, InstallStatus } from '../../../../../types';
import { useCapabilities } from '../../../../../hooks';
import { useUninstallPackage, useGetPackageInstallStatus, useInstallPackage } from '../../../hooks';
import { ConfirmPackageUninstall } from './confirm_package_uninstall';
import { ConfirmPackageInstall } from './confirm_package_install';

View file

@ -0,0 +1,238 @@
/*
* 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, { memo } from 'react';
import styled from 'styled-components';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer } from '@elastic/eui';
import { InstallStatus, PackageInfo } from '../../../../../types';
import { useGetPackagePolicies } from '../../../../../hooks';
import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants';
import { useGetPackageInstallStatus } from '../../../hooks';
import { UpdateIcon } from '../components';
import { InstallationButton } from './installation_button';
const SettingsTitleCell = styled.td`
padding-right: ${(props) => props.theme.eui.spacerSizes.xl};
padding-bottom: ${(props) => props.theme.eui.spacerSizes.m};
`;
const UpdatesAvailableMsgContainer = styled.span`
padding-left: ${(props) => props.theme.eui.spacerSizes.s};
`;
const NoteLabel = () => (
<FormattedMessage
id="xpack.fleet.integrations.settings.packageUninstallNoteDescription.packageUninstallNoteLabel"
defaultMessage="Note:"
/>
);
const UpdatesAvailableMsg = () => (
<UpdatesAvailableMsgContainer>
<UpdateIcon size="l" />
<FormattedMessage
id="xpack.fleet.integrations.settings.versionInfo.updatesAvailable"
defaultMessage="Updates are available"
/>
</UpdatesAvailableMsgContainer>
);
interface Props {
packageInfo: PackageInfo;
}
export const SettingsPage: React.FC<Props> = memo(({ packageInfo }: Props) => {
const { name, title, removable, latestVersion, version } = packageInfo;
const getPackageInstallStatus = useGetPackageInstallStatus();
const { data: packagePoliciesData } = useGetPackagePolicies({
perPage: 0,
page: 1,
kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${name}`,
});
const { status: installationStatus, version: installedVersion } = getPackageInstallStatus(name);
const packageHasUsages = !!packagePoliciesData?.total;
const updateAvailable = installedVersion && installedVersion < latestVersion ? true : false;
const isViewingOldPackage = version < latestVersion;
// hide install/remove options if the user has version of the package is installed
// and this package is out of date or if they do have a version installed but it's not this one
const hideInstallOptions =
(installationStatus === InstallStatus.notInstalled && isViewingOldPackage) ||
(installationStatus === InstallStatus.installed && installedVersion !== version);
const isUpdating = installationStatus === InstallStatus.installing && installedVersion;
return (
<EuiFlexGroup alignItems="flexStart">
<EuiFlexItem grow={1} />
<EuiFlexItem grow={6}>
<EuiText>
<EuiTitle>
<h3>
<FormattedMessage
id="xpack.fleet.integrations.settings.packageSettingsTitle"
defaultMessage="Settings"
/>
</h3>
</EuiTitle>
<EuiSpacer size="s" />
{installedVersion !== null && (
<div>
<EuiTitle>
<h4>
<FormattedMessage
id="xpack.fleet.integrations.settings.packageVersionTitle"
defaultMessage="{title} version"
values={{
title,
}}
/>
</h4>
</EuiTitle>
<EuiSpacer size="s" />
<table>
<tbody>
<tr>
<SettingsTitleCell>
<FormattedMessage
id="xpack.fleet.integrations.settings.versionInfo.installedVersion"
defaultMessage="Installed version"
/>
</SettingsTitleCell>
<td>
<EuiTitle size="xs">
<span>{installedVersion}</span>
</EuiTitle>
{updateAvailable && <UpdatesAvailableMsg />}
</td>
</tr>
<tr>
<SettingsTitleCell>
<FormattedMessage
id="xpack.fleet.integrations.settings.versionInfo.latestVersion"
defaultMessage="Latest version"
/>
</SettingsTitleCell>
<td>
<EuiTitle size="xs">
<span>{latestVersion}</span>
</EuiTitle>
</td>
</tr>
</tbody>
</table>
{updateAvailable && (
<p>
<InstallationButton
{...packageInfo}
version={latestVersion}
disabled={false}
isUpdate={true}
/>
</p>
)}
</div>
)}
{!hideInstallOptions && !isUpdating && (
<div>
<EuiSpacer size="s" />
{installationStatus === InstallStatus.notInstalled ||
installationStatus === InstallStatus.installing ? (
<div>
<EuiTitle>
<h4>
<FormattedMessage
id="xpack.fleet.integrations.settings.packageInstallTitle"
defaultMessage="Install {title}"
values={{
title,
}}
/>
</h4>
</EuiTitle>
<EuiSpacer size="s" />
<p>
<FormattedMessage
id="xpack.fleet.integrations.settings.packageInstallDescription"
defaultMessage="Install this integration to setup Kibana and Elasticsearch assets designed for {title} data."
values={{
title,
}}
/>
</p>
</div>
) : (
<div>
<EuiTitle>
<h4>
<FormattedMessage
id="xpack.fleet.integrations.settings.packageUninstallTitle"
defaultMessage="Uninstall {title}"
values={{
title,
}}
/>
</h4>
</EuiTitle>
<EuiSpacer size="s" />
<p>
<FormattedMessage
id="xpack.fleet.integrations.settings.packageUninstallDescription"
defaultMessage="Remove Kibana and Elasticsearch assets that were installed by this integration."
/>
</p>
</div>
)}
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<p>
<InstallationButton
{...packageInfo}
disabled={
!packagePoliciesData || removable === false ? true : packageHasUsages
}
/>
</p>
</EuiFlexItem>
</EuiFlexGroup>
{packageHasUsages && removable === true && (
<p>
<FormattedMessage
id="xpack.fleet.integrations.settings.packageUninstallNoteDescription.packageUninstallNoteDetail"
defaultMessage="{strongNote} {title} cannot be uninstalled because there are active agents that use this integration. To uninstall, remove all {title} integrations from your agent policies."
values={{
title,
strongNote: (
<strong>
<NoteLabel />
</strong>
),
}}
/>
</p>
)}
{removable === false && (
<p>
<FormattedMessage
id="xpack.fleet.integrations.settings.packageUninstallNoteDescription.packageUninstallUninstallableNoteDetail"
defaultMessage="{strongNote} The {title} integration is installed by default and cannot be removed."
values={{
title,
strongNote: (
<strong>
<NoteLabel />
</strong>
),
}}
/>
</p>
)}
</div>
)}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
});

View file

@ -1,230 +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 React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { EuiSpacer } from '@elastic/eui';
import styled from 'styled-components';
import { InstallStatus, PackageInfo } from '../../../../types';
import { useGetPackagePolicies } from '../../../../hooks';
import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../constants';
import { useGetPackageInstallStatus } from '../../hooks';
import { InstallationButton } from './installation_button';
import { UpdateIcon } from '../../components/icons';
const SettingsTitleCell = styled.td`
padding-right: ${(props) => props.theme.eui.spacerSizes.xl};
padding-bottom: ${(props) => props.theme.eui.spacerSizes.m};
`;
const UpdatesAvailableMsgContainer = styled.span`
padding-left: ${(props) => props.theme.eui.spacerSizes.s};
`;
const NoteLabel = () => (
<FormattedMessage
id="xpack.fleet.integrations.settings.packageUninstallNoteDescription.packageUninstallNoteLabel"
defaultMessage="Note:"
/>
);
const UpdatesAvailableMsg = () => (
<UpdatesAvailableMsgContainer>
<UpdateIcon size="l" />
<FormattedMessage
id="xpack.fleet.integrations.settings.versionInfo.updatesAvailable"
defaultMessage="Updates are available"
/>
</UpdatesAvailableMsgContainer>
);
export const SettingsPanel = (
props: Pick<PackageInfo, 'assets' | 'name' | 'title' | 'version' | 'removable' | 'latestVersion'>
) => {
const { name, title, removable, latestVersion, version } = props;
const getPackageInstallStatus = useGetPackageInstallStatus();
const { data: packagePoliciesData } = useGetPackagePolicies({
perPage: 0,
page: 1,
kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${props.name}`,
});
const { status: installationStatus, version: installedVersion } = getPackageInstallStatus(name);
const packageHasUsages = !!packagePoliciesData?.total;
const updateAvailable = installedVersion && installedVersion < latestVersion ? true : false;
const isViewingOldPackage = version < latestVersion;
// hide install/remove options if the user has version of the package is installed
// and this package is out of date or if they do have a version installed but it's not this one
const hideInstallOptions =
(installationStatus === InstallStatus.notInstalled && isViewingOldPackage) ||
(installationStatus === InstallStatus.installed && installedVersion !== version);
const isUpdating = installationStatus === InstallStatus.installing && installedVersion;
return (
<EuiText>
<EuiTitle>
<h3>
<FormattedMessage
id="xpack.fleet.integrations.settings.packageSettingsTitle"
defaultMessage="Settings"
/>
</h3>
</EuiTitle>
<EuiSpacer size="s" />
{installedVersion !== null && (
<div>
<EuiTitle>
<h4>
<FormattedMessage
id="xpack.fleet.integrations.settings.packageVersionTitle"
defaultMessage="{title} version"
values={{
title,
}}
/>
</h4>
</EuiTitle>
<EuiSpacer size="s" />
<table>
<tbody>
<tr>
<SettingsTitleCell>
<FormattedMessage
id="xpack.fleet.integrations.settings.versionInfo.installedVersion"
defaultMessage="Installed version"
/>
</SettingsTitleCell>
<td>
<EuiTitle size="xs">
<span>{installedVersion}</span>
</EuiTitle>
{updateAvailable && <UpdatesAvailableMsg />}
</td>
</tr>
<tr>
<SettingsTitleCell>
<FormattedMessage
id="xpack.fleet.integrations.settings.versionInfo.latestVersion"
defaultMessage="Latest version"
/>
</SettingsTitleCell>
<td>
<EuiTitle size="xs">
<span>{latestVersion}</span>
</EuiTitle>
</td>
</tr>
</tbody>
</table>
{updateAvailable && (
<p>
<InstallationButton
{...props}
version={latestVersion}
disabled={false}
isUpdate={true}
/>
</p>
)}
</div>
)}
{!hideInstallOptions && !isUpdating && (
<div>
<EuiSpacer size="s" />
{installationStatus === InstallStatus.notInstalled ||
installationStatus === InstallStatus.installing ? (
<div>
<EuiTitle>
<h4>
<FormattedMessage
id="xpack.fleet.integrations.settings.packageInstallTitle"
defaultMessage="Install {title}"
values={{
title,
}}
/>
</h4>
</EuiTitle>
<EuiSpacer size="s" />
<p>
<FormattedMessage
id="xpack.fleet.integrations.settings.packageInstallDescription"
defaultMessage="Install this integration to setup Kibana and Elasticsearch assets designed for {title} data."
values={{
title,
}}
/>
</p>
</div>
) : (
<div>
<EuiTitle>
<h4>
<FormattedMessage
id="xpack.fleet.integrations.settings.packageUninstallTitle"
defaultMessage="Uninstall {title}"
values={{
title,
}}
/>
</h4>
</EuiTitle>
<EuiSpacer size="s" />
<p>
<FormattedMessage
id="xpack.fleet.integrations.settings.packageUninstallDescription"
defaultMessage="Remove Kibana and Elasticsearch assets that were installed by this integration."
/>
</p>
</div>
)}
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<p>
<InstallationButton
{...props}
disabled={!packagePoliciesData || removable === false ? true : packageHasUsages}
/>
</p>
</EuiFlexItem>
</EuiFlexGroup>
{packageHasUsages && removable === true && (
<p>
<FormattedMessage
id="xpack.fleet.integrations.settings.packageUninstallNoteDescription.packageUninstallNoteDetail"
defaultMessage="{strongNote} {title} cannot be uninstalled because there are active agents that use this integration. To uninstall, remove all {title} integrations from your agent policies."
values={{
title,
strongNote: (
<strong>
<NoteLabel />
</strong>
),
}}
/>
</p>
)}
{removable === false && (
<p>
<FormattedMessage
id="xpack.fleet.integrations.settings.packageUninstallNoteDescription.packageUninstallUninstallableNoteDetail"
defaultMessage="{strongNote} The {title} integration is installed by default and cannot be removed."
values={{
title,
strongNote: (
<strong>
<NoteLabel />
</strong>
),
}}
/>
</p>
)}
</div>
)}
</EuiText>
);
};

View file

@ -49,6 +49,7 @@ export {
CreatePackagePolicyResponse,
UpdatePackagePolicyRequest,
UpdatePackagePolicyResponse,
GetPackagePoliciesResponse,
// API schemas - Data streams
GetDataStreamsResponse,
// API schemas - Agents
@ -122,6 +123,7 @@ export {
InstallationStatus,
Installable,
RegistryRelease,
PackageSpecCategory,
} from '../../../../common';
export * from './intra_app_route_state';

View file

@ -32,10 +32,7 @@ export const FleetTrustedAppsCard = memo<PackageCustomExtensionComponentProps>((
const trustedAppsListUrlPath = getTrustedAppsListPath();
const trustedAppRouteState = useMemo<TrustedAppsListPageRouteState>(() => {
const fleetPackageCustomUrlPath = `#${pagePathGetters.integration_details({
pkgkey,
panel: 'custom',
})}`;
const fleetPackageCustomUrlPath = `#${pagePathGetters.integration_details_custom({ pkgkey })}`;
return {
backButtonLabel: i18n.translate(
'xpack.securitySolution.endpoint.fleetCustomExtension.backButtonLabel',

View file

@ -18,7 +18,7 @@ export function FleetIntegrations({ getService, getPageObjects }: FtrProviderCon
return {
async navigateToIntegrationDetails(pkgkey: string) {
await pageObjects.common.navigateToApp(PLUGIN_ID, {
hash: pagePathGetters.integration_details({ pkgkey }),
hash: pagePathGetters.integration_details_overview({ pkgkey }),
});
},