[Fleet] Display NOTICE.txt from package if it exists (#101663)

This commit is contained in:
Nicolas Chaulet 2021-06-14 12:33:46 -04:00 committed by GitHub
parent adda72edd2
commit 91b804f505
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 155 additions and 22 deletions

View file

@ -48,7 +48,12 @@ export type EpmPackageInstallStatus = 'installed' | 'installing';
export type DetailViewPanelName = 'overview' | 'policies' | 'settings' | 'custom';
export type ServiceName = 'kibana' | 'elasticsearch';
export type AgentAssetType = typeof agentAssetTypes;
export type AssetType = KibanaAssetType | ElasticsearchAssetType | ValueOf<AgentAssetType>;
export type DocAssetType = 'doc' | 'notice';
export type AssetType =
| KibanaAssetType
| ElasticsearchAssetType
| ValueOf<AgentAssetType>
| DocAssetType;
/*
Enum mapping of a saved object asset type to how it would appear in a package file path (snake cased)
@ -344,6 +349,7 @@ export interface EpmPackageAdditions {
latestVersion: string;
assets: AssetsGroupedByServiceByType;
removable?: boolean;
notice?: string;
}
type Merge<FirstType, SecondType> = Omit<FirstType, Extract<keyof FirstType, keyof SecondType>> &

View file

@ -7,7 +7,7 @@
import type { IconType } from '@elastic/eui';
import type { AssetType, ServiceName } from '../../types';
import type { ServiceName } from '../../types';
import { ElasticsearchAssetType, KibanaAssetType } from '../../types';
export * from '../../constants';
@ -20,8 +20,9 @@ export const DisplayedAssets: ServiceNameToAssetTypes = {
kibana: Object.values(KibanaAssetType),
elasticsearch: Object.values(ElasticsearchAssetType),
};
export type DisplayedAssetType = KibanaAssetType | ElasticsearchAssetType;
export const AssetTitleMap: Record<AssetType, string> = {
export const AssetTitleMap: Record<DisplayedAssetType, string> = {
dashboard: 'Dashboard',
ilm_policy: 'ILM Policy',
ingest_pipeline: 'Ingest Pipeline',
@ -31,7 +32,6 @@ export const AssetTitleMap: Record<AssetType, string> = {
component_template: 'Component Template',
search: 'Saved Search',
visualization: 'Visualization',
input: 'Agent input',
map: 'Map',
data_stream_ilm_policy: 'Data Stream ILM Policy',
lens: 'Lens',

View file

@ -7,7 +7,7 @@
import type { IconType } from '@elastic/eui';
import type { AssetType, ServiceName } from '../../types';
import type { ServiceName } from '../../types';
import { ElasticsearchAssetType, KibanaAssetType } from '../../types';
// only allow Kibana assets for the kibana key, ES asssets for elasticsearch, etc
@ -19,7 +19,9 @@ export const DisplayedAssets: ServiceNameToAssetTypes = {
elasticsearch: Object.values(ElasticsearchAssetType),
};
export const AssetTitleMap: Record<AssetType, string> = {
export type DisplayedAssetType = ElasticsearchAssetType | KibanaAssetType;
export const AssetTitleMap: Record<DisplayedAssetType, string> = {
dashboard: 'Dashboard',
ilm_policy: 'ILM Policy',
ingest_pipeline: 'Ingest Pipeline',
@ -29,7 +31,6 @@ export const AssetTitleMap: Record<AssetType, string> = {
component_template: 'Component Template',
search: 'Saved Search',
visualization: 'Visualization',
input: 'Agent input',
map: 'Map',
data_stream_ilm_policy: 'Data Stream ILM Policy',
lens: 'Lens',

View file

@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { memo, useMemo } from 'react';
import React, { memo, useCallback, useMemo, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiFlexGroup,
@ -13,6 +13,8 @@ import {
EuiTextColor,
EuiDescriptionList,
EuiNotificationBadge,
EuiLink,
EuiPortal,
} from '@elastic/eui';
import type { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list';
@ -26,6 +28,8 @@ import { entries } from '../../../../../types';
import { useGetCategories } from '../../../../../hooks';
import { AssetTitleMap, DisplayedAssets, ServiceTitleMap } from '../../../constants';
import { NoticeModal } from './notice_modal';
interface Props {
packageInfo: PackageInfo;
}
@ -41,6 +45,11 @@ export const Details: React.FC<Props> = memo(({ packageInfo }) => {
return [];
}, [categoriesData, isLoadingCategories, packageInfo.categories]);
const [isNoticeModalOpen, setIsNoticeModalOpen] = useState(false);
const toggleNoticeModal = useCallback(() => {
setIsNoticeModalOpen(!isNoticeModalOpen);
}, [isNoticeModalOpen]);
const listItems = useMemo(() => {
// Base details: version and categories
const items: EuiDescriptionListProps['listItems'] = [
@ -123,14 +132,23 @@ export const Details: React.FC<Props> = memo(({ packageInfo }) => {
}
// License details
if (packageInfo.license) {
if (packageInfo.license || packageInfo.notice) {
items.push({
title: (
<EuiTextColor color="subdued">
<FormattedMessage id="xpack.fleet.epm.licenseLabel" defaultMessage="License" />
</EuiTextColor>
),
description: packageInfo.license,
description: (
<>
<p>{packageInfo.license}</p>
{packageInfo.notice && (
<p>
<EuiLink onClick={toggleNoticeModal}>NOTICE.txt</EuiLink>
</p>
)}
</>
),
});
}
@ -140,21 +158,30 @@ export const Details: React.FC<Props> = memo(({ packageInfo }) => {
packageInfo.assets,
packageInfo.data_streams,
packageInfo.license,
packageInfo.notice,
packageInfo.version,
toggleNoticeModal,
]);
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>
<>
<EuiPortal>
{isNoticeModalOpen && packageInfo.notice && (
<NoticeModal noticePath={packageInfo.notice} onClose={toggleNoticeModal} />
)}
</EuiPortal>
<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,79 @@
/*
* 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, { useEffect, useState } from 'react';
import {
EuiCodeBlock,
EuiLoadingContent,
EuiModal,
EuiModalBody,
EuiModalHeader,
EuiModalFooter,
EuiModalHeaderTitle,
EuiButton,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { sendGetFileByPath, useStartServices } from '../../../../../hooks';
interface Props {
noticePath: string;
onClose: () => void;
}
export const NoticeModal: React.FunctionComponent<Props> = ({ noticePath, onClose }) => {
const { notifications } = useStartServices();
const [notice, setNotice] = useState<string | undefined>(undefined);
useEffect(() => {
async function fetchData() {
try {
const { data } = await sendGetFileByPath(noticePath);
setNotice(data || '');
} catch (err) {
notifications.toasts.addError(err, {
title: i18n.translate('xpack.fleet.epm.errorLoadingNotice', {
defaultMessage: 'Error loading NOTICE.txt',
}),
});
}
}
fetchData();
}, [noticePath, notifications]);
return (
<EuiModal maxWidth={true} onClose={onClose}>
<EuiModalHeader>
<EuiModalHeaderTitle>
<h1>NOTICE.txt</h1>
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiCodeBlock overflowHeight={360}>
{notice ? (
notice
) : (
// Simulate a long notice while loading
<>
<p>
<EuiLoadingContent lines={5} />
</p>
<p>
<EuiLoadingContent lines={6} />
</p>
</>
)}
</EuiCodeBlock>
</EuiModalBody>
<EuiModalFooter>
<EuiButton color="primary" fill onClick={onClose}>
<FormattedMessage id="xpack.fleet.epm.noticeModalCloseBtn" defaultMessage="Close" />
</EuiButton>
</EuiModalFooter>
</EuiModal>
);
};

View file

@ -114,6 +114,13 @@ export function getPathParts(path: string): AssetParts {
[pkgkey, service, type, file] = path.replace(`data_stream/${dataset}/`, '').split('/');
}
// To support the NOTICE asset at the root level
if (service === 'NOTICE.txt') {
file = service;
type = 'notice';
service = '';
}
// This is to cover for the fields.yml files inside the "fields" directory
if (file === undefined) {
file = type;

View file

@ -126,6 +126,7 @@ export async function getPackageInfo(options: {
title: packageInfo.title || nameAsTitle(packageInfo.name),
assets: Registry.groupPathsByService(paths || []),
removable: !isRequiredPackage(pkgName),
notice: Registry.getNoticePath(paths || []),
};
const updated = { ...packageInfo, ...additions };

View file

@ -255,3 +255,15 @@ export function groupPathsByService(paths: string[]): AssetsGroupedByServiceByTy
elasticsearch: assets.elasticsearch,
};
}
export function getNoticePath(paths: string[]): string | undefined {
for (const path of paths) {
const parts = getPathParts(path.replace(/^\/package\//, ''));
if (parts.type === 'notice') {
const { pkgName, pkgVersion } = splitPkgKey(parts.pkgkey);
return `/package/${pkgName}/${pkgVersion}/${parts.file}`;
}
}
return undefined;
}