mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Fleet] Display NOTICE.txt from package if it exists (#101663)
This commit is contained in:
parent
adda72edd2
commit
91b804f505
8 changed files with 155 additions and 22 deletions
|
@ -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>> &
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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 };
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue