mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Fleet] Show all integration assets on detail page (#182180)
## Summary Resolves https://github.com/elastic/kibana/issues/160555. This PR allows all assets to be shown on Integration details > Assets tab and links them to other apps in Kibana whenever possible for viewing. Previously, only dashboards, visualizations, and saved searches were shown in this view. Now all Kibana and Elasticsearch assets are shown if the integration was installed in the user's current space. If an integration was installed in a different space, only ES assets will be shown. #### Caveats 1) This page lists all assets tracked on the package installation saved object *after* the package is installed (`installed_es` and `installed_kibana`). This list differs from the summary of assets shown on the Overview tab because it includes Fleet-installed assets that are not part of the package, notably index templates, component templates, and default ingest pipelines. https://github.com/elastic/kibana/issues/182197 was created to decide how to resolve this asset count discrepency. 2) ML and Security assets are shown but not linked. The following issues have been created for downstream teams to decide where their assets should link to: #182199, #182200 ### Screenshots <details> <summary>Nginx (including in a different space)</summary>   </details> <details> <summary>Security Posture Management</summary>  </details> <details> <summary>Rapid7 Threat Command</summary>  </details> <details> <summary>Lateral Movement Detection</summary>  </details> ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
c5c40980cb
commit
980d8bf301
21 changed files with 617 additions and 549 deletions
|
@ -4,8 +4,8 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type { AllowedAssetTypes } from '../types/models';
|
||||
import { ElasticsearchAssetType, KibanaAssetType } from '../types/models';
|
||||
import type { DisplayedAssetTypes } from '../types/models';
|
||||
import { ElasticsearchAssetType, KibanaSavedObjectType } from '../types/models';
|
||||
|
||||
export const PACKAGES_SAVED_OBJECT_TYPE = 'epm-packages';
|
||||
export const ASSETS_SAVED_OBJECT_TYPE = 'epm-packages-assets';
|
||||
|
@ -87,11 +87,11 @@ export const installationStatuses = {
|
|||
NotInstalled: 'not_installed',
|
||||
} as const;
|
||||
|
||||
export const allowedAssetTypes: AllowedAssetTypes = [
|
||||
KibanaAssetType.dashboard,
|
||||
KibanaAssetType.search,
|
||||
KibanaAssetType.visualization,
|
||||
ElasticsearchAssetType.transform,
|
||||
// These asset types are allowed to be shown on Integration details > Assets tab
|
||||
// This array also controls the order in which the asset types are displayed
|
||||
export const displayedAssetTypes: DisplayedAssetTypes = [
|
||||
...Object.values(KibanaSavedObjectType),
|
||||
...Object.values(ElasticsearchAssetType),
|
||||
];
|
||||
|
||||
export const allowedAssetTypesLookup = new Set<string>(allowedAssetTypes);
|
||||
export const displayedAssetTypesLookup = new Set<string>(displayedAssetTypes);
|
||||
|
|
|
@ -195,7 +195,7 @@ export type {
|
|||
FleetServerAgentComponentStatus,
|
||||
AssetSOObject,
|
||||
SimpleSOAssetType,
|
||||
AllowedAssetTypes,
|
||||
DisplayedAssetTypes,
|
||||
} from './types';
|
||||
|
||||
export { ElasticsearchAssetType } from './types';
|
||||
|
|
|
@ -6295,33 +6295,33 @@
|
|||
"type": "object",
|
||||
"deprecated": true,
|
||||
"properties": {
|
||||
"response": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/components/schemas/saved_object_type"
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"attributes": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/components/schemas/saved_object_type"
|
||||
},
|
||||
"updatedAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"attributes": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appLink": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3971,26 +3971,26 @@ components:
|
|||
type: object
|
||||
deprecated: true
|
||||
properties:
|
||||
response:
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
type:
|
||||
$ref: '#/components/schemas/saved_object_type'
|
||||
updatedAt:
|
||||
type: string
|
||||
attributes:
|
||||
type: object
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
type:
|
||||
$ref: '#/components/schemas/saved_object_type'
|
||||
updatedAt:
|
||||
type: string
|
||||
attributes:
|
||||
type: object
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
appLink:
|
||||
type: string
|
||||
required:
|
||||
- items
|
||||
get_categories_response:
|
||||
|
|
|
@ -2,25 +2,25 @@ title: Bulk get assets response
|
|||
type: object
|
||||
deprecated: true
|
||||
properties:
|
||||
response:
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
type:
|
||||
$ref: ./saved_object_type.yaml
|
||||
updatedAt:
|
||||
type: string
|
||||
attributes:
|
||||
type: object
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
type:
|
||||
$ref: ./saved_object_type.yaml
|
||||
updatedAt:
|
||||
type: string
|
||||
attributes:
|
||||
type: object
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
appLink:
|
||||
type: string
|
||||
required:
|
||||
- items
|
||||
|
|
|
@ -53,17 +53,17 @@ export type AssetType =
|
|||
*/
|
||||
export enum KibanaAssetType {
|
||||
dashboard = 'dashboard',
|
||||
lens = 'lens',
|
||||
visualization = 'visualization',
|
||||
search = 'search',
|
||||
indexPattern = 'index_pattern',
|
||||
map = 'map',
|
||||
lens = 'lens',
|
||||
mlModule = 'ml_module',
|
||||
securityRule = 'security_rule',
|
||||
cloudSecurityPostureRuleTemplate = 'csp_rule_template',
|
||||
mlModule = 'ml_module',
|
||||
tag = 'tag',
|
||||
osqueryPackAsset = 'osquery_pack_asset',
|
||||
osquerySavedQuery = 'osquery_saved_query',
|
||||
tag = 'tag',
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -71,27 +71,27 @@ export enum KibanaAssetType {
|
|||
*/
|
||||
export enum KibanaSavedObjectType {
|
||||
dashboard = 'dashboard',
|
||||
lens = 'lens',
|
||||
visualization = 'visualization',
|
||||
search = 'search',
|
||||
indexPattern = 'index-pattern',
|
||||
map = 'map',
|
||||
lens = 'lens',
|
||||
mlModule = 'ml-module',
|
||||
securityRule = 'security-rule',
|
||||
cloudSecurityPostureRuleTemplate = 'csp-rule-template',
|
||||
tag = 'tag',
|
||||
osqueryPackAsset = 'osquery-pack-asset',
|
||||
osquerySavedQuery = 'osquery-saved-query',
|
||||
tag = 'tag',
|
||||
}
|
||||
|
||||
export enum ElasticsearchAssetType {
|
||||
index = 'index',
|
||||
indexTemplate = 'index_template',
|
||||
componentTemplate = 'component_template',
|
||||
ingestPipeline = 'ingest_pipeline',
|
||||
indexTemplate = 'index_template',
|
||||
ilmPolicy = 'ilm_policy',
|
||||
transform = 'transform',
|
||||
dataStreamIlmPolicy = 'data_stream_ilm_policy',
|
||||
transform = 'transform',
|
||||
mlModel = 'ml_model',
|
||||
}
|
||||
export type FleetElasticsearchAssetType = Exclude<
|
||||
|
@ -99,12 +99,7 @@ export type FleetElasticsearchAssetType = Exclude<
|
|||
ElasticsearchAssetType.index
|
||||
>;
|
||||
|
||||
export type AllowedAssetTypes = [
|
||||
KibanaAssetType.dashboard,
|
||||
KibanaAssetType.search,
|
||||
KibanaAssetType.visualization,
|
||||
ElasticsearchAssetType.transform
|
||||
];
|
||||
export type DisplayedAssetTypes = Array<`${KibanaSavedObjectType | ElasticsearchAssetType}`>;
|
||||
|
||||
// Defined as part of the removing public references to saved object schemas
|
||||
export interface SimpleSOAssetType {
|
||||
|
@ -112,6 +107,7 @@ export interface SimpleSOAssetType {
|
|||
type: ElasticsearchAssetType | KibanaSavedObjectType;
|
||||
updatedAt?: string;
|
||||
attributes: {
|
||||
service?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
|
|
@ -208,7 +208,7 @@ export interface GetBulkAssetsRequest {
|
|||
}
|
||||
|
||||
export interface GetBulkAssetsResponse {
|
||||
items: SimpleSOAssetType[];
|
||||
items: Array<SimpleSOAssetType & { appLink?: string }>;
|
||||
}
|
||||
|
||||
export interface GetInputsTemplatesRequest {
|
||||
|
|
|
@ -1,59 +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 { AssetsFacetGroup as Component } from './assets_facet_group';
|
||||
|
||||
export default {
|
||||
component: Component,
|
||||
title: 'Sections/EPM/Assets Facet Group',
|
||||
};
|
||||
|
||||
interface Args {
|
||||
width: number;
|
||||
}
|
||||
|
||||
const args: Args = {
|
||||
width: 250,
|
||||
};
|
||||
|
||||
export const AssetsFacetGroup = ({ width }: Args) => {
|
||||
return (
|
||||
<div style={{ width }}>
|
||||
<Component
|
||||
assets={{
|
||||
kibana: {
|
||||
csp_rule_template: [],
|
||||
dashboard: [],
|
||||
visualization: [],
|
||||
index_pattern: [],
|
||||
search: [],
|
||||
map: [],
|
||||
lens: [],
|
||||
security_rule: [],
|
||||
ml_module: [],
|
||||
tag: [],
|
||||
osquery_pack_asset: [],
|
||||
osquery_saved_query: [],
|
||||
},
|
||||
elasticsearch: {
|
||||
component_template: [],
|
||||
data_stream_ilm_policy: [],
|
||||
ilm_policy: [],
|
||||
index_template: [],
|
||||
ingest_pipeline: [],
|
||||
transform: [],
|
||||
ml_model: [],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AssetsFacetGroup.args = args;
|
|
@ -1,115 +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 {
|
||||
EuiFacetButton,
|
||||
EuiFacetGroup,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiText,
|
||||
EuiTextColor,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import type {
|
||||
AssetsGroupedByServiceByType,
|
||||
AssetTypeToParts,
|
||||
KibanaAssetType,
|
||||
} from '../../../types';
|
||||
import { entries } from '../../../types';
|
||||
import {
|
||||
AssetIcons,
|
||||
AssetTitleMap,
|
||||
DisplayedAssets,
|
||||
ServiceIcons,
|
||||
ServiceTitleMap,
|
||||
} from '../constants';
|
||||
|
||||
const FirstHeaderRow = styled(EuiFlexGroup)`
|
||||
padding: 0 0 ${(props) => props.theme.eui.euiSizeM} 0;
|
||||
`;
|
||||
|
||||
const HeaderRow = styled(EuiFlexGroup)`
|
||||
padding: ${(props) => props.theme.eui.euiSizeM} 0;
|
||||
`;
|
||||
|
||||
const FacetGroup = styled(EuiFacetGroup)`
|
||||
flex-grow: 0;
|
||||
`;
|
||||
|
||||
const FacetButton = styled(EuiFacetButton)`
|
||||
&&& {
|
||||
.euiFacetButton__icon,
|
||||
.euiFacetButton__quantity {
|
||||
opacity: 1;
|
||||
}
|
||||
.euiFacetButton__text {
|
||||
color: ${(props) => props.theme.eui.euiTextColor};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function AssetsFacetGroup({ assets }: { assets: AssetsGroupedByServiceByType }) {
|
||||
return (
|
||||
<Fragment>
|
||||
{entries(assets).map(([service, typeToParts], index) => {
|
||||
const Header = index === 0 ? FirstHeaderRow : HeaderRow;
|
||||
// filter out assets we are not going to display
|
||||
const filteredTypes: AssetTypeToParts = entries(typeToParts).reduce(
|
||||
(acc: any, [asset, value]) => {
|
||||
if (DisplayedAssets[service].includes(asset)) acc[asset] = value;
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
return (
|
||||
<Fragment key={service}>
|
||||
<Header gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type={ServiceIcons[service]} />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiTitle key={service} size="xs">
|
||||
<EuiText>
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.epm.assetGroupTitle"
|
||||
defaultMessage="{assetType} assets"
|
||||
values={{
|
||||
assetType: ServiceTitleMap[service],
|
||||
}}
|
||||
/>
|
||||
</h4>
|
||||
</EuiText>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</Header>
|
||||
|
||||
<FacetGroup>
|
||||
{entries(filteredTypes).map(([_type, parts]) => {
|
||||
const type = _type as KibanaAssetType;
|
||||
// only kibana assets have icons
|
||||
const iconType = type in AssetIcons && AssetIcons[type];
|
||||
const iconNode = iconType ? <EuiIcon type={iconType} size="s" /> : '';
|
||||
return (
|
||||
<FacetButton key={type} quantity={parts.length} icon={iconNode} isDisabled={true}>
|
||||
<EuiTextColor color="subdued">{AssetTitleMap[type]}</EuiTextColor>
|
||||
</FacetButton>
|
||||
);
|
||||
})}
|
||||
</FacetGroup>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
|
@ -5,27 +5,89 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { IconType } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import type { ServiceName } from '../../types';
|
||||
import type { ServiceName, KibanaSavedObjectType } from '../../types';
|
||||
import { ElasticsearchAssetType, KibanaAssetType } from '../../types';
|
||||
|
||||
// only allow Kibana assets for the kibana key, ES assets for elasticsearch, etc
|
||||
type ServiceNameToAssetTypes = Record<Extract<ServiceName, 'kibana'>, KibanaAssetType[]> &
|
||||
Record<Extract<ServiceName, 'elasticsearch'>, ElasticsearchAssetType[]>;
|
||||
|
||||
export const DisplayedAssets: ServiceNameToAssetTypes = {
|
||||
export const DisplayedAssetsFromPackageInfo: ServiceNameToAssetTypes = {
|
||||
kibana: Object.values(KibanaAssetType),
|
||||
elasticsearch: Object.values(ElasticsearchAssetType),
|
||||
};
|
||||
|
||||
export type DisplayedAssetType = ElasticsearchAssetType | KibanaAssetType | 'view';
|
||||
|
||||
export const AssetTitleMap: Record<DisplayedAssetType, string> = {
|
||||
export const AssetTitleMap: Record<
|
||||
KibanaSavedObjectType | KibanaAssetType | ElasticsearchAssetType | 'view',
|
||||
string
|
||||
> = {
|
||||
// Kibana
|
||||
// Duplication is because some assets are listed from package paths (snake cased)
|
||||
// and some are from saved objects (kebab cased)
|
||||
dashboard: i18n.translate('xpack.fleet.epm.assetTitles.dashboards', {
|
||||
defaultMessage: 'Dashboards',
|
||||
}),
|
||||
lens: i18n.translate('xpack.fleet.epm.assetTitles.lens', {
|
||||
defaultMessage: 'Lens',
|
||||
}),
|
||||
visualization: i18n.translate('xpack.fleet.epm.assetTitles.visualizations', {
|
||||
defaultMessage: 'Visualizations',
|
||||
}),
|
||||
search: i18n.translate('xpack.fleet.epm.assetTitles.savedSearches', {
|
||||
defaultMessage: 'Saved searches',
|
||||
}),
|
||||
'index-pattern': i18n.translate('xpack.fleet.epm.assetTitles.indexPatterns', {
|
||||
defaultMessage: 'Data views',
|
||||
}),
|
||||
index_pattern: i18n.translate('xpack.fleet.epm.assetTitles.indexPatterns', {
|
||||
defaultMessage: 'Data views',
|
||||
}),
|
||||
map: i18n.translate('xpack.fleet.epm.assetTitles.maps', {
|
||||
defaultMessage: 'Maps',
|
||||
}),
|
||||
'security-rule': i18n.translate('xpack.fleet.epm.assetTitles.securityRules', {
|
||||
defaultMessage: 'Security rules',
|
||||
}),
|
||||
security_rule: i18n.translate('xpack.fleet.epm.assetTitles.securityRules', {
|
||||
defaultMessage: 'Security rules',
|
||||
}),
|
||||
'csp-rule-template': i18n.translate(
|
||||
'xpack.fleet.epm.assetTitles.cloudSecurityPostureRuleTemplate',
|
||||
{
|
||||
defaultMessage: 'Benchmark rules',
|
||||
}
|
||||
),
|
||||
csp_rule_template: i18n.translate(
|
||||
'xpack.fleet.epm.assetTitles.cloudSecurityPostureRuleTemplate',
|
||||
{
|
||||
defaultMessage: 'Benchmark rules',
|
||||
}
|
||||
),
|
||||
'ml-module': i18n.translate('xpack.fleet.epm.assetTitles.mlModules', {
|
||||
defaultMessage: 'ML modules',
|
||||
}),
|
||||
ml_module: i18n.translate('xpack.fleet.epm.assetTitles.mlModules', {
|
||||
defaultMessage: 'ML modules',
|
||||
}),
|
||||
tag: i18n.translate('xpack.fleet.epm.assetTitles.tag', {
|
||||
defaultMessage: 'Tags',
|
||||
}),
|
||||
'osquery-pack-asset': i18n.translate('xpack.fleet.epm.assetTitles.osqueryPackAssets', {
|
||||
defaultMessage: 'Osquery packs',
|
||||
}),
|
||||
osquery_pack_asset: i18n.translate('xpack.fleet.epm.assetTitles.osqueryPackAssets', {
|
||||
defaultMessage: 'Osquery packs',
|
||||
}),
|
||||
'osquery-saved-query': i18n.translate('xpack.fleet.epm.assetTitles.osquerySavedQuery', {
|
||||
defaultMessage: 'Osquery saved queries',
|
||||
}),
|
||||
osquery_saved_query: i18n.translate('xpack.fleet.epm.assetTitles.osquerySavedQuery', {
|
||||
defaultMessage: 'Osquery saved queries',
|
||||
}),
|
||||
|
||||
// ES
|
||||
ilm_policy: i18n.translate('xpack.fleet.epm.assetTitles.ilmPolicies', {
|
||||
defaultMessage: 'ILM policies',
|
||||
}),
|
||||
|
@ -38,80 +100,24 @@ export const AssetTitleMap: Record<DisplayedAssetType, string> = {
|
|||
index: i18n.translate('xpack.fleet.epm.assetTitles.indices', {
|
||||
defaultMessage: 'Indices',
|
||||
}),
|
||||
index_pattern: i18n.translate('xpack.fleet.epm.assetTitles.indexPatterns', {
|
||||
defaultMessage: 'Index patterns',
|
||||
}),
|
||||
index_template: i18n.translate('xpack.fleet.epm.assetTitles.indexTemplates', {
|
||||
defaultMessage: 'Index templates',
|
||||
}),
|
||||
component_template: i18n.translate('xpack.fleet.epm.assetTitles.componentTemplates', {
|
||||
defaultMessage: 'Component templates',
|
||||
}),
|
||||
search: i18n.translate('xpack.fleet.epm.assetTitles.savedSearches', {
|
||||
defaultMessage: 'Saved searches',
|
||||
}),
|
||||
visualization: i18n.translate('xpack.fleet.epm.assetTitles.visualizations', {
|
||||
defaultMessage: 'Visualizations',
|
||||
}),
|
||||
map: i18n.translate('xpack.fleet.epm.assetTitles.maps', {
|
||||
defaultMessage: 'Maps',
|
||||
}),
|
||||
data_stream_ilm_policy: i18n.translate('xpack.fleet.epm.assetTitles.dataStreamILM', {
|
||||
defaultMessage: 'Data stream ILM policies',
|
||||
}),
|
||||
lens: i18n.translate('xpack.fleet.epm.assetTitles.lens', {
|
||||
defaultMessage: 'Lens',
|
||||
}),
|
||||
security_rule: i18n.translate('xpack.fleet.epm.assetTitles.securityRules', {
|
||||
defaultMessage: 'Security rules',
|
||||
}),
|
||||
osquery_pack_asset: i18n.translate('xpack.fleet.epm.assetTitles.osqueryPackAssets', {
|
||||
defaultMessage: 'Osquery packs',
|
||||
}),
|
||||
osquery_saved_query: i18n.translate('xpack.fleet.epm.assetTitles.osquerySavedQuery', {
|
||||
defaultMessage: 'Osquery saved queries',
|
||||
}),
|
||||
ml_module: i18n.translate('xpack.fleet.epm.assetTitles.mlModules', {
|
||||
defaultMessage: 'ML modules',
|
||||
}),
|
||||
ml_model: i18n.translate('xpack.fleet.epm.assetTitles.mlModels', {
|
||||
defaultMessage: 'ML models',
|
||||
}),
|
||||
view: i18n.translate('xpack.fleet.epm.assetTitles.views', {
|
||||
defaultMessage: 'Views',
|
||||
}),
|
||||
tag: i18n.translate('xpack.fleet.epm.assetTitles.tag', {
|
||||
defaultMessage: 'Tag',
|
||||
}),
|
||||
csp_rule_template: i18n.translate(
|
||||
'xpack.fleet.epm.assetTitles.cloudSecurityPostureRuleTemplate',
|
||||
{
|
||||
defaultMessage: 'Benchmark rules',
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
export const ServiceTitleMap: Record<ServiceName, string> = {
|
||||
kibana: 'Kibana',
|
||||
elasticsearch: 'Elasticsearch',
|
||||
};
|
||||
|
||||
export const AssetIcons: Record<KibanaAssetType, IconType> = {
|
||||
dashboard: 'dashboardApp',
|
||||
index_pattern: 'indexPatternApp',
|
||||
search: 'searchProfilerApp',
|
||||
visualization: 'visualizeApp',
|
||||
map: 'emsApp',
|
||||
lens: 'lensApp',
|
||||
security_rule: 'securityApp',
|
||||
csp_rule_template: 'securityApp', // TODO ICON
|
||||
ml_module: 'mlApp',
|
||||
tag: 'tagApp',
|
||||
osquery_pack_asset: 'osqueryApp',
|
||||
osquery_saved_query: 'osqueryApp',
|
||||
};
|
||||
|
||||
export const ServiceIcons: Record<ServiceName, IconType> = {
|
||||
elasticsearch: 'logoElasticsearch',
|
||||
kibana: 'logoKibana',
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { Fragment, useEffect, useState, useCallback } from 'react';
|
||||
import React, { Fragment, useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiTitle, EuiCallOut } from '@elastic/eui';
|
||||
|
@ -13,14 +13,15 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiTitle, EuiCallOut } f
|
|||
import type {
|
||||
EsAssetReference,
|
||||
AssetSOObject,
|
||||
KibanaAssetReference,
|
||||
SimpleSOAssetType,
|
||||
} from '../../../../../../../../common';
|
||||
import { allowedAssetTypes } from '../../../../../../../../common/constants';
|
||||
import { displayedAssetTypes } from '../../../../../../../../common/constants';
|
||||
|
||||
import { Error, ExtensionWrapper, Loading } from '../../../../../components';
|
||||
|
||||
import type { PackageInfo } from '../../../../../types';
|
||||
import { ElasticsearchAssetType, InstallStatus } from '../../../../../types';
|
||||
import { InstallStatus } from '../../../../../types';
|
||||
|
||||
import {
|
||||
useGetPackageInstallStatus,
|
||||
|
@ -28,6 +29,7 @@ import {
|
|||
useStartServices,
|
||||
useUIExtension,
|
||||
useAuthz,
|
||||
useFleetStatus,
|
||||
} from '../../../../../hooks';
|
||||
|
||||
import { sendGetBulkAssets } from '../../../../../hooks';
|
||||
|
@ -45,7 +47,9 @@ export const AssetsPage = ({ packageInfo, refetchPackageInfo }: AssetsPanelProps
|
|||
const { name, version } = packageInfo;
|
||||
|
||||
const pkgkey = `${name}-${version}`;
|
||||
const { spaces, docLinks } = useStartServices();
|
||||
const { docLinks } = useStartServices();
|
||||
const { spaceId } = useFleetStatus();
|
||||
|
||||
const customAssetsExtension = useUIExtension(packageInfo.name, 'package-detail-assets');
|
||||
|
||||
const canReadPackageSettings = useAuthz().integrations.readPackageInfo;
|
||||
|
@ -54,11 +58,38 @@ export const AssetsPage = ({ packageInfo, refetchPackageInfo }: AssetsPanelProps
|
|||
const getPackageInstallStatus = useGetPackageInstallStatus();
|
||||
const packageInstallStatus = getPackageInstallStatus(packageInfo.name);
|
||||
|
||||
// assume assets are installed in this space until we find otherwise
|
||||
const [assetsInstalledInCurrentSpace, setAssetsInstalledInCurrentSpace] = useState<boolean>(true);
|
||||
const [assetSavedObjects, setAssetsSavedObjects] = useState<undefined | SimpleSOAssetType[]>();
|
||||
const [deferredInstallations, setDeferredInstallations] = useState<EsAssetReference[]>();
|
||||
const pkgInstallationInfo =
|
||||
'installationInfo' in packageInfo ? packageInfo.installationInfo : undefined;
|
||||
|
||||
const installedSpaceId = pkgInstallationInfo?.installed_kibana_space_id;
|
||||
const assetsInstalledInCurrentSpace = !installedSpaceId || installedSpaceId === spaceId;
|
||||
|
||||
const [assetSavedObjectsByType, setAssetsSavedObjectsByType] = useState<
|
||||
Record<string, Record<string, SimpleSOAssetType & { appLink?: string }>>
|
||||
>({});
|
||||
const [deferredInstallations, setDeferredInstallations] = useState<EsAssetReference[]>();
|
||||
const pkgAssets = useMemo(
|
||||
() => [
|
||||
...(assetsInstalledInCurrentSpace ? pkgInstallationInfo?.installed_kibana || [] : []),
|
||||
...(pkgInstallationInfo?.installed_es || []),
|
||||
],
|
||||
[
|
||||
assetsInstalledInCurrentSpace,
|
||||
pkgInstallationInfo?.installed_es,
|
||||
pkgInstallationInfo?.installed_kibana,
|
||||
]
|
||||
);
|
||||
const pkgAssetsByType = useMemo(
|
||||
() =>
|
||||
pkgAssets.reduce((acc, asset) => {
|
||||
if (!acc[asset.type] && displayedAssetTypes.includes(asset.type)) {
|
||||
acc[asset.type] = [];
|
||||
}
|
||||
acc[asset.type].push(asset);
|
||||
return acc;
|
||||
}, {} as Record<string, Array<EsAssetReference | KibanaAssetReference>>),
|
||||
[pkgAssets]
|
||||
);
|
||||
const [fetchError, setFetchError] = useState<undefined | Error>();
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
|
||||
|
@ -70,65 +101,51 @@ export const AssetsPage = ({ packageInfo, refetchPackageInfo }: AssetsPanelProps
|
|||
|
||||
useEffect(() => {
|
||||
const fetchAssetSavedObjects = async () => {
|
||||
if ('installationInfo' in packageInfo) {
|
||||
if (spaces) {
|
||||
const { id: spaceId } = await spaces.getActiveSpace();
|
||||
const assetInstallSpaceId = packageInfo.installationInfo?.installed_kibana_space_id;
|
||||
if (!pkgInstallationInfo) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// if assets are installed in a different space no need to attempt to load them.
|
||||
if (assetInstallSpaceId && assetInstallSpaceId !== spaceId) {
|
||||
setAssetsInstalledInCurrentSpace(false);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (pkgAssets.length === 0) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const pkgInstallationInfo = packageInfo.installationInfo;
|
||||
if (pkgAssets.length > 0) {
|
||||
const deferredAssets = pkgAssets.filter((asset): asset is EsAssetReference => {
|
||||
return 'deferred' in asset && asset.deferred === true;
|
||||
});
|
||||
setDeferredInstallations(deferredAssets);
|
||||
}
|
||||
|
||||
if (
|
||||
pkgInstallationInfo?.installed_es &&
|
||||
Array.isArray(pkgInstallationInfo.installed_es) &&
|
||||
pkgInstallationInfo.installed_es.length > 0
|
||||
) {
|
||||
const deferredAssets = pkgInstallationInfo.installed_es.filter(
|
||||
(asset) => asset.deferred === true
|
||||
try {
|
||||
const assetIds: AssetSOObject[] = pkgAssets.map(({ id, type }) => ({
|
||||
id,
|
||||
type,
|
||||
}));
|
||||
|
||||
const { data, error } = await sendGetBulkAssets({ assetIds });
|
||||
if (error) {
|
||||
setFetchError(error);
|
||||
} else {
|
||||
setAssetsSavedObjectsByType(
|
||||
(data?.items || []).reduce((acc, asset) => {
|
||||
if (!acc[asset.type]) {
|
||||
acc[asset.type] = {};
|
||||
}
|
||||
acc[asset.type][asset.id] = asset;
|
||||
return acc;
|
||||
}, {} as typeof assetSavedObjectsByType)
|
||||
);
|
||||
setDeferredInstallations(deferredAssets);
|
||||
}
|
||||
const authorizedTransforms = (pkgInstallationInfo?.installed_es || []).filter(
|
||||
(asset) => asset.type === ElasticsearchAssetType.transform && !asset.deferred
|
||||
);
|
||||
|
||||
if (
|
||||
authorizedTransforms?.length === 0 &&
|
||||
(!pkgInstallationInfo?.installed_kibana ||
|
||||
pkgInstallationInfo.installed_kibana.length === 0)
|
||||
) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const assetIds: AssetSOObject[] = [
|
||||
...authorizedTransforms,
|
||||
...(pkgInstallationInfo?.installed_kibana || []),
|
||||
].map(({ id, type }) => ({
|
||||
id,
|
||||
type,
|
||||
}));
|
||||
|
||||
const response = await sendGetBulkAssets({ assetIds });
|
||||
setAssetsSavedObjects(response.data?.items);
|
||||
} catch (e) {
|
||||
setFetchError(e);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
} else {
|
||||
} catch (e) {
|
||||
setFetchError(e);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchAssetSavedObjects();
|
||||
}, [packageInfo, spaces]);
|
||||
}, [packageInfo, pkgAssets, pkgInstallationInfo]);
|
||||
|
||||
// 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
|
||||
|
@ -136,8 +153,9 @@ export const AssetsPage = ({ packageInfo, refetchPackageInfo }: AssetsPanelProps
|
|||
return <Redirect to={getPath('integration_details_overview', { pkgkey })} />;
|
||||
}
|
||||
|
||||
const showDeferredInstallations =
|
||||
const hasDeferredInstallations =
|
||||
Array.isArray(deferredInstallations) && deferredInstallations.length > 0;
|
||||
|
||||
let content: JSX.Element | Array<JSX.Element | null> | null;
|
||||
if (isLoading) {
|
||||
content = <Loading />;
|
||||
|
@ -158,58 +176,18 @@ export const AssetsPage = ({ packageInfo, refetchPackageInfo }: AssetsPanelProps
|
|||
/>
|
||||
</EuiCallOut>
|
||||
);
|
||||
} else if (fetchError) {
|
||||
content = (
|
||||
<Error
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.epm.packageDetails.assets.fetchAssetsErrorTitle"
|
||||
defaultMessage="Error loading assets"
|
||||
/>
|
||||
}
|
||||
error={fetchError}
|
||||
/>
|
||||
);
|
||||
} else if (!assetsInstalledInCurrentSpace) {
|
||||
content = (
|
||||
<EuiCallOut
|
||||
heading="h2"
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.epm.packageDetails.assets.assetsNotAvailableInCurrentSpaceTitle"
|
||||
defaultMessage="Assets not available in this space"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.epm.packageDetails.assets.assetsNotAvailableInCurrentSpaceBody"
|
||||
defaultMessage="This integration is installed, but no assets are available in this space. {learnMoreLink}."
|
||||
values={{
|
||||
learnMoreLink: (
|
||||
<EuiLink href={docLinks.links.fleet.installAndUninstallIntegrationAssets} external>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.epm.packageDetails.assets.assetsNotAvailableInCurrentSpace.learnMore"
|
||||
defaultMessage="Learn more"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
);
|
||||
} else if (assetSavedObjects === undefined || assetSavedObjects.length === 0) {
|
||||
} else if (pkgAssets.length === 0) {
|
||||
if (customAssetsExtension) {
|
||||
// If a UI extension for custom asset entries is defined, render the custom component here despite
|
||||
// there being no saved objects found
|
||||
content = (
|
||||
<ExtensionWrapper>
|
||||
<customAssetsExtension.Component />
|
||||
<EuiSpacer size="l" />
|
||||
</ExtensionWrapper>
|
||||
);
|
||||
} else {
|
||||
content = !showDeferredInstallations ? (
|
||||
content = !hasDeferredInstallations ? (
|
||||
<EuiTitle>
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
|
@ -222,33 +200,76 @@ export const AssetsPage = ({ packageInfo, refetchPackageInfo }: AssetsPanelProps
|
|||
}
|
||||
} else {
|
||||
content = [
|
||||
...allowedAssetTypes.map((assetType) => {
|
||||
const sectionAssetSavedObjects = assetSavedObjects.filter((so) => so.type === assetType);
|
||||
// Show callout if Kibana assets are installed in a different space
|
||||
!assetsInstalledInCurrentSpace ? (
|
||||
<>
|
||||
<EuiCallOut
|
||||
heading="h2"
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.epm.packageDetails.assets.assetsNotAvailableInCurrentSpaceTitle"
|
||||
defaultMessage="Kibana assets not available in this space"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.epm.packageDetails.assets.assetsNotAvailableInCurrentSpaceBody"
|
||||
defaultMessage="This integration is installed, but Kibana assets are not available in this space. {learnMoreLink}."
|
||||
values={{
|
||||
learnMoreLink: (
|
||||
<EuiLink
|
||||
href={docLinks.links.fleet.installAndUninstallIntegrationAssets}
|
||||
external
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.epm.packageDetails.assets.assetsNotAvailableInCurrentSpace.learnMore"
|
||||
defaultMessage="Learn more"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
|
||||
if (!sectionAssetSavedObjects.length) {
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
) : null,
|
||||
|
||||
// Ensure we add any custom assets provided via UI extension to the before other assets
|
||||
customAssetsExtension ? (
|
||||
<ExtensionWrapper>
|
||||
<customAssetsExtension.Component />
|
||||
<EuiSpacer size="l" />
|
||||
</ExtensionWrapper>
|
||||
) : null,
|
||||
|
||||
// List all assets by order of `displayedAssetTypes`
|
||||
...displayedAssetTypes.map((assetType) => {
|
||||
const assets = pkgAssetsByType[assetType] || [];
|
||||
const soAssets = assetSavedObjectsByType[assetType] || {};
|
||||
const finalAssets = assets.map((asset) => {
|
||||
return {
|
||||
...asset,
|
||||
...soAssets[asset.id],
|
||||
};
|
||||
});
|
||||
|
||||
if (!finalAssets.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment key={assetType}>
|
||||
<AssetsAccordion
|
||||
savedObjects={sectionAssetSavedObjects}
|
||||
type={assetType}
|
||||
key={assetType}
|
||||
/>
|
||||
<AssetsAccordion savedObjects={finalAssets} type={assetType} key={assetType} />
|
||||
<EuiSpacer size="l" />
|
||||
</Fragment>
|
||||
);
|
||||
}),
|
||||
// Ensure we add any custom assets provided via UI extension to the end of the list of other assets
|
||||
customAssetsExtension ? (
|
||||
<ExtensionWrapper>
|
||||
<customAssetsExtension.Component />
|
||||
</ExtensionWrapper>
|
||||
) : null,
|
||||
];
|
||||
}
|
||||
const deferredInstallationsContent = showDeferredInstallations ? (
|
||||
const deferredInstallationsContent = hasDeferredInstallations ? (
|
||||
<>
|
||||
<DeferredAssetsSection
|
||||
deferredInstallations={deferredInstallations}
|
||||
|
@ -263,6 +284,20 @@ export const AssetsPage = ({ packageInfo, refetchPackageInfo }: AssetsPanelProps
|
|||
<EuiFlexGroup alignItems="flexStart">
|
||||
<EuiFlexItem grow={1} />
|
||||
<EuiFlexItem grow={6}>
|
||||
{fetchError && (
|
||||
<>
|
||||
<Error
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.epm.packageDetails.assets.fetchAssetsErrorTitle"
|
||||
defaultMessage="Error loading complete asset information"
|
||||
/>
|
||||
}
|
||||
error={fetchError}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
)}
|
||||
{deferredInstallationsContent}
|
||||
{content}
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -22,20 +22,16 @@ import {
|
|||
} from '@elastic/eui';
|
||||
|
||||
import { AssetTitleMap } from '../../../constants';
|
||||
import type { DisplayedAssetTypes, GetBulkAssetsResponse } from '../../../../../../../../common';
|
||||
import { useStartServices } from '../../../../../hooks';
|
||||
import { KibanaAssetType } from '../../../../../types';
|
||||
|
||||
import type { SimpleSOAssetType, AllowedAssetTypes } from '../../../../../../../../common';
|
||||
export type DisplayedAssetType = DisplayedAssetTypes[number] | 'view';
|
||||
|
||||
import { getHrefToObjectInKibanaApp, useStartServices } from '../../../../../hooks';
|
||||
|
||||
import { ElasticsearchAssetType, KibanaAssetType } from '../../../../../types';
|
||||
|
||||
export type AllowedAssetType = AllowedAssetTypes[number] | 'view';
|
||||
interface Props {
|
||||
type: AllowedAssetType;
|
||||
savedObjects: SimpleSOAssetType[];
|
||||
}
|
||||
|
||||
export const AssetsAccordion: FunctionComponent<Props> = ({ savedObjects, type }) => {
|
||||
export const AssetsAccordion: FunctionComponent<{
|
||||
type: DisplayedAssetType;
|
||||
savedObjects: GetBulkAssetsResponse['items'];
|
||||
}> = ({ savedObjects, type }) => {
|
||||
const { http } = useStartServices();
|
||||
|
||||
const isDashboard = type === KibanaAssetType.dashboard;
|
||||
|
@ -62,25 +58,21 @@ export const AssetsAccordion: FunctionComponent<Props> = ({ savedObjects, type }
|
|||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiSplitPanel.Outer hasBorder hasShadow={false}>
|
||||
{savedObjects.map(({ id, attributes: { title: soTitle, description } }, idx) => {
|
||||
{savedObjects.map(({ id, attributes, appLink }, idx) => {
|
||||
const { title: soTitle, description } = attributes || {};
|
||||
// Ignore custom asset views or if not a Kibana asset
|
||||
if (type === 'view') {
|
||||
return;
|
||||
}
|
||||
|
||||
const pathToObjectInApp = getHrefToObjectInKibanaApp({
|
||||
http,
|
||||
id,
|
||||
type: type === ElasticsearchAssetType.transform ? undefined : type,
|
||||
});
|
||||
const title = soTitle ?? id;
|
||||
return (
|
||||
<Fragment key={id}>
|
||||
<EuiSplitPanel.Inner grow={false} key={idx}>
|
||||
<EuiText size="m">
|
||||
<p>
|
||||
{pathToObjectInApp ? (
|
||||
<EuiLink href={pathToObjectInApp}>{title}</EuiLink>
|
||||
{appLink ? (
|
||||
<EuiLink href={http.basePath.prepend(appLink)}>{title}</EuiLink>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
|
|
|
@ -33,7 +33,7 @@ import type {
|
|||
} from '../../../../../types';
|
||||
import { entries } from '../../../../../types';
|
||||
import { useGetCategoriesQuery } from '../../../../../hooks';
|
||||
import { AssetTitleMap, DisplayedAssets, ServiceTitleMap } from '../../../constants';
|
||||
import { AssetTitleMap, DisplayedAssetsFromPackageInfo, ServiceTitleMap } from '../../../constants';
|
||||
|
||||
import { ChangelogModal } from '../settings/changelog_modal';
|
||||
|
||||
|
@ -133,7 +133,7 @@ export const Details: React.FC<Props> = memo(({ packageInfo, integrationInfo })
|
|||
// (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;
|
||||
if (DisplayedAssetsFromPackageInfo[service].includes(asset)) acc[asset] = value;
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
|
|
|
@ -5,10 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useContext, useState } from 'react';
|
||||
import React, { useContext, useState, useEffect } from 'react';
|
||||
|
||||
import type { GetFleetStatusResponse } from '../types';
|
||||
|
||||
import { useStartServices } from './use_core';
|
||||
import { useConfig } from './use_config';
|
||||
import { useGetFleetStatusQuery } from './use_request';
|
||||
|
||||
|
@ -20,6 +21,7 @@ export interface FleetStatusProviderProps {
|
|||
missingRequirements?: GetFleetStatusResponse['missing_requirements'];
|
||||
missingOptionalFeatures?: GetFleetStatusResponse['missing_optional_features'];
|
||||
isSecretsStorageEnabled?: GetFleetStatusResponse['is_secrets_storage_enabled'];
|
||||
spaceId?: string;
|
||||
}
|
||||
|
||||
interface FleetStatus extends FleetStatusProviderProps {
|
||||
|
@ -39,9 +41,20 @@ export const FleetStatusProvider: React.FC<{
|
|||
defaultFleetStatus?: FleetStatusProviderProps;
|
||||
}> = ({ defaultFleetStatus, children }) => {
|
||||
const config = useConfig();
|
||||
const { spaces } = useStartServices();
|
||||
const [spaceId, setSpaceId] = useState<string | undefined>();
|
||||
const [forceDisplayInstructions, setForceDisplayInstructions] = useState(false);
|
||||
|
||||
const { data, isLoading, refetch } = useGetFleetStatusQuery();
|
||||
useEffect(() => {
|
||||
const getSpace = async () => {
|
||||
if (spaces) {
|
||||
const space = await spaces.getActiveSpace();
|
||||
setSpaceId(space.id);
|
||||
}
|
||||
};
|
||||
getSpace();
|
||||
}, [spaces]);
|
||||
|
||||
const state = {
|
||||
...defaultFleetStatus,
|
||||
|
@ -51,6 +64,7 @@ export const FleetStatusProvider: React.FC<{
|
|||
missingRequirements: data?.missing_requirements,
|
||||
missingOptionalFeatures: data?.missing_optional_features,
|
||||
isSecretsStorageEnabled: data?.is_secrets_storage_enabled,
|
||||
spaceId,
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -6,8 +6,6 @@
|
|||
*/
|
||||
import type { HttpStart } from '@kbn/core/public';
|
||||
|
||||
import { KibanaAssetType } from '../types';
|
||||
|
||||
import { useStartServices } from '.';
|
||||
|
||||
const KIBANA_BASE_PATH = '/app/kibana';
|
||||
|
@ -16,44 +14,6 @@ const getKibanaLink = (http: HttpStart, path: string) => {
|
|||
return http.basePath.prepend(`${KIBANA_BASE_PATH}#${path}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* TODO: This is a temporary solution for getting links to various assets. It is very risky because:
|
||||
*
|
||||
* 1. The plugin might not exist/be enabled
|
||||
* 2. URLs and paths might not always be supported
|
||||
*
|
||||
* We should migrate to using the new URL service locators.
|
||||
*
|
||||
* @deprecated {@link Locators} from the new URL service need to be used instead.
|
||||
|
||||
*/
|
||||
export const getHrefToObjectInKibanaApp = ({
|
||||
type,
|
||||
id,
|
||||
http,
|
||||
}: {
|
||||
type: KibanaAssetType | undefined;
|
||||
id: string;
|
||||
http: HttpStart;
|
||||
}): undefined | string => {
|
||||
let kibanaAppPath: undefined | string;
|
||||
switch (type) {
|
||||
case KibanaAssetType.dashboard:
|
||||
kibanaAppPath = `/dashboard/${id}`;
|
||||
break;
|
||||
case KibanaAssetType.search:
|
||||
kibanaAppPath = `/discover/${id}`;
|
||||
break;
|
||||
case KibanaAssetType.visualization:
|
||||
kibanaAppPath = `/visualize/edit/${id}`;
|
||||
break;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return getKibanaLink(http, kibanaAppPath);
|
||||
};
|
||||
|
||||
/**
|
||||
* TODO: This functionality needs to be replaced with use of the new URL service locators
|
||||
*
|
||||
|
|
|
@ -237,10 +237,16 @@ export const getBulkAssetsHandler: FleetRequestHandler<
|
|||
undefined,
|
||||
TypeOf<typeof GetBulkAssetsRequestSchema.body>
|
||||
> = async (context, request, response) => {
|
||||
const coreContext = await context.core;
|
||||
try {
|
||||
const { assetIds } = request.body;
|
||||
const savedObjectsClient = (await context.fleet).internalSoClient;
|
||||
const assets = await getBulkAssets(savedObjectsClient, assetIds as AssetSOObject[]);
|
||||
const savedObjectsClient = coreContext.savedObjects.client;
|
||||
const savedObjectsTypeRegistry = coreContext.savedObjects.typeRegistry;
|
||||
const assets = await getBulkAssets(
|
||||
savedObjectsClient,
|
||||
savedObjectsTypeRegistry,
|
||||
assetIds as AssetSOObject[]
|
||||
);
|
||||
|
||||
const body: GetBulkAssetsResponse = {
|
||||
items: assets,
|
||||
|
|
|
@ -5,33 +5,87 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { SavedObjectsClientContract } from '@kbn/core/server';
|
||||
|
||||
import type {
|
||||
AssetSOObject,
|
||||
ElasticsearchAssetType,
|
||||
KibanaSavedObjectType,
|
||||
SimpleSOAssetType,
|
||||
} from '../../../../common';
|
||||
SavedObjectsClientContract,
|
||||
ISavedObjectTypeRegistry,
|
||||
SavedObjectsType,
|
||||
} from '@kbn/core/server';
|
||||
|
||||
import { allowedAssetTypesLookup } from '../../../../common/constants';
|
||||
import type { AssetSOObject, KibanaSavedObjectType, SimpleSOAssetType } from '../../../../common';
|
||||
import { ElasticsearchAssetType } from '../../../../common';
|
||||
|
||||
import { displayedAssetTypesLookup } from '../../../../common/constants';
|
||||
|
||||
import type { SimpleSOAssetAttributes } from '../../../types';
|
||||
|
||||
const getKibanaLinkForESAsset = (type: ElasticsearchAssetType, id: string): string => {
|
||||
switch (type) {
|
||||
case 'index':
|
||||
return `/app/management/data/index_management/indices/index_details?indexName=${id}`;
|
||||
case 'index_template':
|
||||
return `/app/management/data/index_management/templates/${id}`;
|
||||
case 'component_template':
|
||||
return `/app/management/data/index_management/component_templates/${id}`;
|
||||
case 'ingest_pipeline':
|
||||
return `/app/management/ingest/ingest_pipelines/?pipeline=${id}`;
|
||||
case 'ilm_policy':
|
||||
return `/app/management/data/index_lifecycle_management/policies/edit/${id}`;
|
||||
case 'data_stream_ilm_policy':
|
||||
return `/app/management/data/index_lifecycle_management/policies/edit/${id}`;
|
||||
case 'transform':
|
||||
// TODO: Confirm link for transforms
|
||||
return '';
|
||||
case 'ml_model':
|
||||
// TODO: Confirm link for ml models
|
||||
return '';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export async function getBulkAssets(
|
||||
soClient: SavedObjectsClientContract,
|
||||
soTypeRegistry: ISavedObjectTypeRegistry,
|
||||
assetIds: AssetSOObject[]
|
||||
) {
|
||||
const { resolved_objects: resolvedObjects } = await soClient.bulkResolve<SimpleSOAssetAttributes>(
|
||||
assetIds
|
||||
);
|
||||
const types: Record<string, SavedObjectsType | undefined> = {};
|
||||
|
||||
const res: SimpleSOAssetType[] = resolvedObjects
|
||||
.map(({ saved_object: savedObject }) => savedObject)
|
||||
.filter(
|
||||
(savedObject) =>
|
||||
savedObject?.error?.statusCode !== 404 && allowedAssetTypesLookup.has(savedObject.type)
|
||||
)
|
||||
.filter((savedObject) => displayedAssetTypesLookup.has(savedObject.type))
|
||||
.map((obj) => {
|
||||
// Kibana SOs are registered with an app URL getter, so try to use that
|
||||
// for retrieving links to assets whenever possible
|
||||
if (!types[obj.type]) {
|
||||
types[obj.type] = soTypeRegistry.getType(obj.type);
|
||||
}
|
||||
let appLink: string = '';
|
||||
try {
|
||||
if (types[obj.type]?.management?.getInAppUrl) {
|
||||
appLink = types[obj.type]!.management!.getInAppUrl!(obj)?.path || '';
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore errors from `getInAppUrl()`
|
||||
// This can happen if user can't access the saved object (i.e. in a different space)
|
||||
}
|
||||
|
||||
// TODO: Ask for Kibana SOs to have `getInAppUrl()` registered so that the above works safely:
|
||||
// ml-module
|
||||
// security-rule
|
||||
// csp-rule-template
|
||||
// osquery-pack-asset
|
||||
// osquery-saved-query
|
||||
|
||||
// If we still don't have an app link at this point, manually map them (only ES types)
|
||||
if (!appLink) {
|
||||
if (Object.values(ElasticsearchAssetType).includes(obj.type as ElasticsearchAssetType)) {
|
||||
appLink = getKibanaLinkForESAsset(obj.type as ElasticsearchAssetType, obj.id);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: obj.id,
|
||||
type: obj.type as unknown as ElasticsearchAssetType | KibanaSavedObjectType,
|
||||
|
@ -40,6 +94,7 @@ export async function getBulkAssets(
|
|||
title: obj.attributes?.title,
|
||||
description: obj.attributes?.description,
|
||||
},
|
||||
appLink,
|
||||
};
|
||||
});
|
||||
return res;
|
||||
|
|
|
@ -32,6 +32,7 @@ import type {
|
|||
KafkaPartitionType,
|
||||
KafkaSaslMechanism,
|
||||
KafkaTopicWhenType,
|
||||
SimpleSOAssetType,
|
||||
} from '../../common/types';
|
||||
|
||||
export type AgentPolicyStatus = typeof agentPolicyStatuses;
|
||||
|
@ -241,7 +242,4 @@ export interface DownloadSourceSOAttributes {
|
|||
source_id?: string;
|
||||
proxy_id?: string | null;
|
||||
}
|
||||
export interface SimpleSOAssetAttributes {
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
export type SimpleSOAssetAttributes = SimpleSOAssetType['attributes'];
|
||||
|
|
188
x-pack/test/fleet_api_integration/apis/epm/__snapshots__/bulk_get_assets.snap
generated
Normal file
188
x-pack/test/fleet_api_integration/apis/epm/__snapshots__/bulk_get_assets.snap
generated
Normal file
|
@ -0,0 +1,188 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EPM Endpoints Bulk get assets installs all assets when installing a package for the first time should get the assets based on the required objects 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"appLink": "/app/management/data/index_lifecycle_management/policies/edit/all_assets",
|
||||
"attributes": Object {},
|
||||
"id": "all_assets",
|
||||
"type": "ilm_policy",
|
||||
},
|
||||
Object {
|
||||
"appLink": "/app/management/data/index_lifecycle_management/policies/edit/logs-all_assets.test_logs-all_assets",
|
||||
"attributes": Object {},
|
||||
"id": "logs-all_assets.test_logs-all_assets",
|
||||
"type": "data_stream_ilm_policy",
|
||||
},
|
||||
Object {
|
||||
"appLink": "/app/management/data/index_lifecycle_management/policies/edit/metrics-all_assets.test_metrics-all_assets",
|
||||
"attributes": Object {},
|
||||
"id": "metrics-all_assets.test_metrics-all_assets",
|
||||
"type": "data_stream_ilm_policy",
|
||||
},
|
||||
Object {
|
||||
"appLink": "",
|
||||
"attributes": Object {},
|
||||
"id": "default",
|
||||
"type": "ml_model",
|
||||
},
|
||||
Object {
|
||||
"appLink": "/app/management/ingest/ingest_pipelines/?pipeline=logs-all_assets.test_logs-0.1.0",
|
||||
"attributes": Object {},
|
||||
"id": "logs-all_assets.test_logs-0.1.0",
|
||||
"type": "ingest_pipeline",
|
||||
},
|
||||
Object {
|
||||
"appLink": "/app/management/ingest/ingest_pipelines/?pipeline=logs-all_assets.test_logs-0.1.0-pipeline1",
|
||||
"attributes": Object {},
|
||||
"id": "logs-all_assets.test_logs-0.1.0-pipeline1",
|
||||
"type": "ingest_pipeline",
|
||||
},
|
||||
Object {
|
||||
"appLink": "/app/management/ingest/ingest_pipelines/?pipeline=logs-all_assets.test_logs-0.1.0-pipeline2",
|
||||
"attributes": Object {},
|
||||
"id": "logs-all_assets.test_logs-0.1.0-pipeline2",
|
||||
"type": "ingest_pipeline",
|
||||
},
|
||||
Object {
|
||||
"appLink": "/app/management/ingest/ingest_pipelines/?pipeline=metrics-all_assets.test_metrics-0.1.0",
|
||||
"attributes": Object {},
|
||||
"id": "metrics-all_assets.test_metrics-0.1.0",
|
||||
"type": "ingest_pipeline",
|
||||
},
|
||||
Object {
|
||||
"appLink": "/app/management/data/index_management/templates/logs-all_assets.test_logs",
|
||||
"attributes": Object {},
|
||||
"id": "logs-all_assets.test_logs",
|
||||
"type": "index_template",
|
||||
},
|
||||
Object {
|
||||
"appLink": "/app/management/data/index_management/component_templates/logs-all_assets.test_logs@package",
|
||||
"attributes": Object {},
|
||||
"id": "logs-all_assets.test_logs@package",
|
||||
"type": "component_template",
|
||||
},
|
||||
Object {
|
||||
"appLink": "/app/management/data/index_management/component_templates/logs-all_assets.test_logs@custom",
|
||||
"attributes": Object {},
|
||||
"id": "logs-all_assets.test_logs@custom",
|
||||
"type": "component_template",
|
||||
},
|
||||
Object {
|
||||
"appLink": "/app/management/data/index_management/templates/metrics-all_assets.test_metrics",
|
||||
"attributes": Object {},
|
||||
"id": "metrics-all_assets.test_metrics",
|
||||
"type": "index_template",
|
||||
},
|
||||
Object {
|
||||
"appLink": "/app/management/data/index_management/component_templates/metrics-all_assets.test_metrics@package",
|
||||
"attributes": Object {},
|
||||
"id": "metrics-all_assets.test_metrics@package",
|
||||
"type": "component_template",
|
||||
},
|
||||
Object {
|
||||
"appLink": "/app/management/data/index_management/component_templates/metrics-all_assets.test_metrics@custom",
|
||||
"attributes": Object {},
|
||||
"id": "metrics-all_assets.test_metrics@custom",
|
||||
"type": "component_template",
|
||||
},
|
||||
Object {
|
||||
"appLink": "/app/dashboards#/view/sample_dashboard",
|
||||
"attributes": Object {
|
||||
"description": "Sample dashboard",
|
||||
"title": "[Logs Sample] Overview ECS",
|
||||
},
|
||||
"id": "sample_dashboard",
|
||||
"type": "dashboard",
|
||||
},
|
||||
Object {
|
||||
"appLink": "/app/dashboards#/view/sample_dashboard2",
|
||||
"attributes": Object {
|
||||
"description": "Sample dashboard 2",
|
||||
"title": "[Logs Sample2] Overview ECS",
|
||||
},
|
||||
"id": "sample_dashboard2",
|
||||
"type": "dashboard",
|
||||
},
|
||||
Object {
|
||||
"appLink": "/app/lens#/edit/sample_lens",
|
||||
"attributes": Object {
|
||||
"description": "",
|
||||
"title": "sample-lens",
|
||||
},
|
||||
"id": "sample_lens",
|
||||
"type": "lens",
|
||||
},
|
||||
Object {
|
||||
"appLink": "/app/visualize#/edit/sample_visualization",
|
||||
"attributes": Object {
|
||||
"description": "sample visualization update",
|
||||
"title": "sample vis title",
|
||||
},
|
||||
"id": "sample_visualization",
|
||||
"type": "visualization",
|
||||
},
|
||||
Object {
|
||||
"appLink": "/app/discover#/view/sample_search",
|
||||
"attributes": Object {
|
||||
"description": "",
|
||||
"title": "All logs [Logs Kafka] ECS",
|
||||
},
|
||||
"id": "sample_search",
|
||||
"type": "search",
|
||||
},
|
||||
Object {
|
||||
"appLink": "/app/management/kibana/dataViews/dataView/test-*",
|
||||
"attributes": Object {
|
||||
"title": "test-*",
|
||||
},
|
||||
"id": "test-*",
|
||||
"type": "index-pattern",
|
||||
},
|
||||
Object {
|
||||
"appLink": "",
|
||||
"attributes": Object {
|
||||
"description": "Find unusual activity in HTTP access logs from filebeat (ECS).",
|
||||
"title": "Nginx access logs",
|
||||
},
|
||||
"id": "sample_ml_module",
|
||||
"type": "ml-module",
|
||||
},
|
||||
Object {
|
||||
"appLink": "",
|
||||
"attributes": Object {
|
||||
"description": "Identifies a suspicious parent child process relationship with cmd.exe descending from svchost.exe",
|
||||
},
|
||||
"id": "sample_security_rule",
|
||||
"type": "security-rule",
|
||||
},
|
||||
Object {
|
||||
"appLink": "",
|
||||
"attributes": Object {},
|
||||
"id": "sample_csp_rule_template",
|
||||
"type": "csp-rule-template",
|
||||
},
|
||||
Object {
|
||||
"appLink": "",
|
||||
"attributes": Object {},
|
||||
"id": "sample_osquery_pack_asset",
|
||||
"type": "osquery-pack-asset",
|
||||
},
|
||||
Object {
|
||||
"appLink": "/app/osquery/saved_queries/sample_osquery_saved_query",
|
||||
"attributes": Object {
|
||||
"description": "Test saved query description",
|
||||
},
|
||||
"id": "sample_osquery_saved_query",
|
||||
"type": "osquery-saved-query",
|
||||
},
|
||||
Object {
|
||||
"appLink": "",
|
||||
"attributes": Object {
|
||||
"description": "",
|
||||
},
|
||||
"id": "sample_tag",
|
||||
"type": "tag",
|
||||
},
|
||||
]
|
||||
`;
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { GetBulkAssetsResponse } from '@kbn/fleet-plugin/common';
|
||||
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
|
||||
import { skipIfNoDockerRegistry } from '../../helpers';
|
||||
|
@ -44,37 +43,30 @@ export default function (providerContext: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('should get the assets based on the required objects', async () => {
|
||||
const packageInfo = await supertest
|
||||
.get(`/api/fleet/epm/packages/${pkgName}/${pkgVersion}`)
|
||||
.expect(200);
|
||||
const packageSOAttributes = packageInfo.body.item.savedObject.attributes;
|
||||
const { body }: { body: GetBulkAssetsResponse } = await supertest
|
||||
.post(`/api/fleet/epm/bulk_assets`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({
|
||||
assetIds: [
|
||||
{
|
||||
type: 'dashboard',
|
||||
id: 'sample_dashboard',
|
||||
},
|
||||
{
|
||||
id: 'sample_visualization',
|
||||
type: 'visualization',
|
||||
},
|
||||
...packageSOAttributes.installed_es,
|
||||
...packageSOAttributes.installed_kibana,
|
||||
],
|
||||
})
|
||||
.expect(200);
|
||||
const asset1 = body.items[0];
|
||||
expect(asset1.id).to.equal('sample_dashboard');
|
||||
expect(asset1.type).to.equal('dashboard');
|
||||
expect(asset1.attributes).to.eql({
|
||||
title: '[Logs Sample] Overview ECS',
|
||||
description: 'Sample dashboard',
|
||||
});
|
||||
|
||||
const asset2 = body.items[1];
|
||||
expect(asset2.id).to.equal('sample_visualization');
|
||||
expect(asset2.type).to.equal('visualization');
|
||||
expect(asset2.attributes).to.eql({
|
||||
title: 'sample vis title',
|
||||
description: 'sample visualization update',
|
||||
});
|
||||
// check overall list of assets and app links
|
||||
expectSnapshot(
|
||||
body.items.map((item) => ({
|
||||
type: item.type,
|
||||
id: item.id,
|
||||
appLink: item.appLink,
|
||||
attributes: item.attributes,
|
||||
}))
|
||||
).toMatch();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -343,6 +343,10 @@ export default function (providerContext: FtrProviderContext) {
|
|||
id: 'sample_dashboard',
|
||||
type: 'dashboard',
|
||||
},
|
||||
{
|
||||
id: 'sample_lens',
|
||||
type: 'lens',
|
||||
},
|
||||
{
|
||||
id: 'sample_visualization',
|
||||
type: 'visualization',
|
||||
|
@ -352,8 +356,8 @@ export default function (providerContext: FtrProviderContext) {
|
|||
type: 'search',
|
||||
},
|
||||
{
|
||||
id: 'sample_lens',
|
||||
type: 'lens',
|
||||
id: 'sample_ml_module',
|
||||
type: 'ml-module',
|
||||
},
|
||||
{
|
||||
id: 'sample_security_rule',
|
||||
|
@ -363,14 +367,6 @@ export default function (providerContext: FtrProviderContext) {
|
|||
id: 'sample_csp_rule_template2',
|
||||
type: 'csp-rule-template',
|
||||
},
|
||||
{
|
||||
id: 'sample_ml_module',
|
||||
type: 'ml-module',
|
||||
},
|
||||
{
|
||||
id: 'sample_tag',
|
||||
type: 'tag',
|
||||
},
|
||||
{
|
||||
id: 'sample_osquery_pack_asset',
|
||||
type: 'osquery-pack-asset',
|
||||
|
@ -379,6 +375,10 @@ export default function (providerContext: FtrProviderContext) {
|
|||
id: 'sample_osquery_saved_query',
|
||||
type: 'osquery-saved-query',
|
||||
},
|
||||
{
|
||||
id: 'sample_tag',
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
installed_es: [
|
||||
{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue