mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -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 DetailViewPanelName = 'overview' | 'policies' | 'settings' | 'custom';
|
||||||
export type ServiceName = 'kibana' | 'elasticsearch';
|
export type ServiceName = 'kibana' | 'elasticsearch';
|
||||||
export type AgentAssetType = typeof agentAssetTypes;
|
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)
|
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;
|
latestVersion: string;
|
||||||
assets: AssetsGroupedByServiceByType;
|
assets: AssetsGroupedByServiceByType;
|
||||||
removable?: boolean;
|
removable?: boolean;
|
||||||
|
notice?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Merge<FirstType, SecondType> = Omit<FirstType, Extract<keyof FirstType, keyof SecondType>> &
|
type Merge<FirstType, SecondType> = Omit<FirstType, Extract<keyof FirstType, keyof SecondType>> &
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
import type { IconType } from '@elastic/eui';
|
import type { IconType } from '@elastic/eui';
|
||||||
|
|
||||||
import type { AssetType, ServiceName } from '../../types';
|
import type { ServiceName } from '../../types';
|
||||||
import { ElasticsearchAssetType, KibanaAssetType } from '../../types';
|
import { ElasticsearchAssetType, KibanaAssetType } from '../../types';
|
||||||
|
|
||||||
export * from '../../constants';
|
export * from '../../constants';
|
||||||
|
@ -20,8 +20,9 @@ export const DisplayedAssets: ServiceNameToAssetTypes = {
|
||||||
kibana: Object.values(KibanaAssetType),
|
kibana: Object.values(KibanaAssetType),
|
||||||
elasticsearch: Object.values(ElasticsearchAssetType),
|
elasticsearch: Object.values(ElasticsearchAssetType),
|
||||||
};
|
};
|
||||||
|
export type DisplayedAssetType = KibanaAssetType | ElasticsearchAssetType;
|
||||||
|
|
||||||
export const AssetTitleMap: Record<AssetType, string> = {
|
export const AssetTitleMap: Record<DisplayedAssetType, string> = {
|
||||||
dashboard: 'Dashboard',
|
dashboard: 'Dashboard',
|
||||||
ilm_policy: 'ILM Policy',
|
ilm_policy: 'ILM Policy',
|
||||||
ingest_pipeline: 'Ingest Pipeline',
|
ingest_pipeline: 'Ingest Pipeline',
|
||||||
|
@ -31,7 +32,6 @@ export const AssetTitleMap: Record<AssetType, string> = {
|
||||||
component_template: 'Component Template',
|
component_template: 'Component Template',
|
||||||
search: 'Saved Search',
|
search: 'Saved Search',
|
||||||
visualization: 'Visualization',
|
visualization: 'Visualization',
|
||||||
input: 'Agent input',
|
|
||||||
map: 'Map',
|
map: 'Map',
|
||||||
data_stream_ilm_policy: 'Data Stream ILM Policy',
|
data_stream_ilm_policy: 'Data Stream ILM Policy',
|
||||||
lens: 'Lens',
|
lens: 'Lens',
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
import type { IconType } from '@elastic/eui';
|
import type { IconType } from '@elastic/eui';
|
||||||
|
|
||||||
import type { AssetType, ServiceName } from '../../types';
|
import type { ServiceName } from '../../types';
|
||||||
import { ElasticsearchAssetType, KibanaAssetType } from '../../types';
|
import { ElasticsearchAssetType, KibanaAssetType } from '../../types';
|
||||||
|
|
||||||
// only allow Kibana assets for the kibana key, ES asssets for elasticsearch, etc
|
// 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),
|
elasticsearch: Object.values(ElasticsearchAssetType),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AssetTitleMap: Record<AssetType, string> = {
|
export type DisplayedAssetType = ElasticsearchAssetType | KibanaAssetType;
|
||||||
|
|
||||||
|
export const AssetTitleMap: Record<DisplayedAssetType, string> = {
|
||||||
dashboard: 'Dashboard',
|
dashboard: 'Dashboard',
|
||||||
ilm_policy: 'ILM Policy',
|
ilm_policy: 'ILM Policy',
|
||||||
ingest_pipeline: 'Ingest Pipeline',
|
ingest_pipeline: 'Ingest Pipeline',
|
||||||
|
@ -29,7 +31,6 @@ export const AssetTitleMap: Record<AssetType, string> = {
|
||||||
component_template: 'Component Template',
|
component_template: 'Component Template',
|
||||||
search: 'Saved Search',
|
search: 'Saved Search',
|
||||||
visualization: 'Visualization',
|
visualization: 'Visualization',
|
||||||
input: 'Agent input',
|
|
||||||
map: 'Map',
|
map: 'Map',
|
||||||
data_stream_ilm_policy: 'Data Stream ILM Policy',
|
data_stream_ilm_policy: 'Data Stream ILM Policy',
|
||||||
lens: 'Lens',
|
lens: 'Lens',
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
import React, { memo, useMemo } from 'react';
|
import React, { memo, useCallback, useMemo, useState } from 'react';
|
||||||
import { FormattedMessage } from '@kbn/i18n/react';
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
import {
|
import {
|
||||||
EuiFlexGroup,
|
EuiFlexGroup,
|
||||||
|
@ -13,6 +13,8 @@ import {
|
||||||
EuiTextColor,
|
EuiTextColor,
|
||||||
EuiDescriptionList,
|
EuiDescriptionList,
|
||||||
EuiNotificationBadge,
|
EuiNotificationBadge,
|
||||||
|
EuiLink,
|
||||||
|
EuiPortal,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import type { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list';
|
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 { useGetCategories } from '../../../../../hooks';
|
||||||
import { AssetTitleMap, DisplayedAssets, ServiceTitleMap } from '../../../constants';
|
import { AssetTitleMap, DisplayedAssets, ServiceTitleMap } from '../../../constants';
|
||||||
|
|
||||||
|
import { NoticeModal } from './notice_modal';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
packageInfo: PackageInfo;
|
packageInfo: PackageInfo;
|
||||||
}
|
}
|
||||||
|
@ -41,6 +45,11 @@ export const Details: React.FC<Props> = memo(({ packageInfo }) => {
|
||||||
return [];
|
return [];
|
||||||
}, [categoriesData, isLoadingCategories, packageInfo.categories]);
|
}, [categoriesData, isLoadingCategories, packageInfo.categories]);
|
||||||
|
|
||||||
|
const [isNoticeModalOpen, setIsNoticeModalOpen] = useState(false);
|
||||||
|
const toggleNoticeModal = useCallback(() => {
|
||||||
|
setIsNoticeModalOpen(!isNoticeModalOpen);
|
||||||
|
}, [isNoticeModalOpen]);
|
||||||
|
|
||||||
const listItems = useMemo(() => {
|
const listItems = useMemo(() => {
|
||||||
// Base details: version and categories
|
// Base details: version and categories
|
||||||
const items: EuiDescriptionListProps['listItems'] = [
|
const items: EuiDescriptionListProps['listItems'] = [
|
||||||
|
@ -123,14 +132,23 @@ export const Details: React.FC<Props> = memo(({ packageInfo }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// License details
|
// License details
|
||||||
if (packageInfo.license) {
|
if (packageInfo.license || packageInfo.notice) {
|
||||||
items.push({
|
items.push({
|
||||||
title: (
|
title: (
|
||||||
<EuiTextColor color="subdued">
|
<EuiTextColor color="subdued">
|
||||||
<FormattedMessage id="xpack.fleet.epm.licenseLabel" defaultMessage="License" />
|
<FormattedMessage id="xpack.fleet.epm.licenseLabel" defaultMessage="License" />
|
||||||
</EuiTextColor>
|
</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.assets,
|
||||||
packageInfo.data_streams,
|
packageInfo.data_streams,
|
||||||
packageInfo.license,
|
packageInfo.license,
|
||||||
|
packageInfo.notice,
|
||||||
packageInfo.version,
|
packageInfo.version,
|
||||||
|
toggleNoticeModal,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiFlexGroup direction="column" gutterSize="m">
|
<>
|
||||||
<EuiFlexItem>
|
<EuiPortal>
|
||||||
<EuiText>
|
{isNoticeModalOpen && packageInfo.notice && (
|
||||||
<h4>
|
<NoticeModal noticePath={packageInfo.notice} onClose={toggleNoticeModal} />
|
||||||
<FormattedMessage id="xpack.fleet.epm.detailsTitle" defaultMessage="Details" />
|
)}
|
||||||
</h4>
|
</EuiPortal>
|
||||||
</EuiText>
|
<EuiFlexGroup direction="column" gutterSize="m">
|
||||||
</EuiFlexItem>
|
<EuiFlexItem>
|
||||||
<EuiFlexItem>
|
<EuiText>
|
||||||
<EuiDescriptionList type="column" compressed listItems={listItems} />
|
<h4>
|
||||||
</EuiFlexItem>
|
<FormattedMessage id="xpack.fleet.epm.detailsTitle" defaultMessage="Details" />
|
||||||
</EuiFlexGroup>
|
</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('/');
|
[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
|
// This is to cover for the fields.yml files inside the "fields" directory
|
||||||
if (file === undefined) {
|
if (file === undefined) {
|
||||||
file = type;
|
file = type;
|
||||||
|
|
|
@ -126,6 +126,7 @@ export async function getPackageInfo(options: {
|
||||||
title: packageInfo.title || nameAsTitle(packageInfo.name),
|
title: packageInfo.title || nameAsTitle(packageInfo.name),
|
||||||
assets: Registry.groupPathsByService(paths || []),
|
assets: Registry.groupPathsByService(paths || []),
|
||||||
removable: !isRequiredPackage(pkgName),
|
removable: !isRequiredPackage(pkgName),
|
||||||
|
notice: Registry.getNoticePath(paths || []),
|
||||||
};
|
};
|
||||||
const updated = { ...packageInfo, ...additions };
|
const updated = { ...packageInfo, ...additions };
|
||||||
|
|
||||||
|
|
|
@ -255,3 +255,15 @@ export function groupPathsByService(paths: string[]): AssetsGroupedByServiceByTy
|
||||||
elasticsearch: assets.elasticsearch,
|
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