[Integrations UI] Add support for custom asset definitions in Integration assets tab (#103554)

* Add UI extension logic for assets + set up custom log views

* Add endpoint security UI extension

* Add synthetics ui extension

* Address PR feedback

- Remove default filter for log stream url
- Fix missing basePath prepend on asset urls
- Expand accordion by default on assetless integrations

* Fix type errors

* Add initial APM extension setup

* Fix missing ExtensionWrapper for enrollment extension

* Fix custom logs asset extension

* Fix type errors

* Add new hook for enrollment flyout ui extensions

* Address PR review + refactor UI extension usage for flyout

* Update limits.yml via script

* Fix type errors

* Add tests for custom assets UI extensions

* Update tests for flyout

* Remove unused import

* Fix type errors in ui extension tests

* Skip view data tests and link to issue

* Use RedirectAppLinks + fix synthetics link

* Use constants for app ID's where possible

* Revert limits.yml

* Fix lazy imports for custom asset components

* Update endpoint custom assets link + description

* Add translation for custom assets UI

* Address PR review in APM

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Kyle Pollich 2021-07-01 18:41:17 -04:00 committed by GitHub
parent 1c82ad2c95
commit 059ed0821a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 671 additions and 163 deletions

View file

@ -3,33 +3,34 @@
"version": "8.0.0", "version": "8.0.0",
"kibanaVersion": "kibana", "kibanaVersion": "kibana",
"requiredPlugins": [ "requiredPlugins": [
"features",
"apmOss", "apmOss",
"data", "data",
"licensing",
"triggersActionsUi",
"embeddable", "embeddable",
"features",
"fleet",
"infra", "infra",
"licensing",
"observability", "observability",
"ruleRegistry" "ruleRegistry",
"triggersActionsUi"
], ],
"optionalPlugins": [ "optionalPlugins": [
"spaces",
"cloud",
"usageCollection",
"taskManager",
"actions", "actions",
"alerting", "alerting",
"security", "cloud",
"ml",
"home", "home",
"maps", "maps",
"fleet" "ml",
"security",
"spaces",
"taskManager",
"usageCollection"
], ],
"server": true, "server": true,
"ui": true, "ui": true,
"configPath": ["xpack", "apm"], "configPath": ["xpack", "apm"],
"requiredBundles": [ "requiredBundles": [
"fleet",
"home", "home",
"kibanaReact", "kibanaReact",
"kibanaUtils", "kibanaUtils",

View file

@ -0,0 +1,36 @@
/*
* 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 { i18n } from '@kbn/i18n';
import {
CustomAssetsAccordionProps,
CustomAssetsAccordion,
} from '../../../../fleet/public';
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
import { ApmPluginStartDeps } from '../../plugin';
export function ApmCustomAssetsExtension() {
const { http } = useKibana<ApmPluginStartDeps>().services;
const basePath = http?.basePath.get();
const views: CustomAssetsAccordionProps['views'] = [
{
name: i18n.translate('xpack.apm.fleetIntegration.assets.name', {
defaultMessage: 'Services',
}),
url: `${basePath}/app/apm`,
description: i18n.translate(
'xpack.apm.fleetIntegration.assets.description',
{ defaultMessage: 'View application traces and service maps in APM' }
),
},
];
return <CustomAssetsAccordion views={views} initialIsOpen />;
}

View file

@ -0,0 +1,58 @@
/*
* 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 { EuiButton, EuiText, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { AgentEnrollmentFlyoutFinalStepExtension } from '../../../../fleet/public';
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
import { ApmPluginStartDeps } from '../../plugin';
function StepComponent() {
const { http } = useKibana<ApmPluginStartDeps>().services;
const installApmAgentLink = http?.basePath.prepend('/app/home#/tutorial/apm');
return (
<>
<EuiText>
<p>
{i18n.translate(
'xpack.apm.fleetIntegration.enrollmentFlyout.installApmAgentDescription',
{
defaultMessage:
'After the agent starts, you can install APM agents on your hosts to collect data from your applications and services.',
}
)}
</p>
</EuiText>
<EuiSpacer size="m" />
<EuiButton fill href={installApmAgentLink}>
{i18n.translate(
'xpack.apm.fleetIntegration.enrollmentFlyout.installApmAgentButtonText',
{ defaultMessage: 'Install APM Agent' }
)}
</EuiButton>
</>
);
}
export function getApmEnrollmentFlyoutData(): Pick<
AgentEnrollmentFlyoutFinalStepExtension,
'title' | 'Component'
> {
return {
title: i18n.translate(
'xpack.apm.fleetIntegration.enrollmentFlyout.installApmAgentTitle',
{
defaultMessage: 'Install APM Agent',
}
),
Component: StepComponent,
};
}

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export * from './apm_enrollment_flyout_extension';
export * from './lazy_apm_custom_assets_extension';

View file

@ -0,0 +1,18 @@
/*
* 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 { lazy } from 'react';
export const LazyApmCustomAssetsExtension = lazy(async () => {
const { ApmCustomAssetsExtension } = await import(
'./apm_custom_assets_extension'
);
return {
default: ApmCustomAssetsExtension,
};
});

View file

@ -22,6 +22,7 @@ import type {
DataPublicPluginStart, DataPublicPluginStart,
} from '../../../../src/plugins/data/public'; } from '../../../../src/plugins/data/public';
import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public';
import type { FleetStart } from '../../fleet/public';
import type { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; import type { HomePublicPluginSetup } from '../../../../src/plugins/home/public';
import type { import type {
PluginSetupContract as AlertingPluginPublicSetup, PluginSetupContract as AlertingPluginPublicSetup,
@ -43,6 +44,10 @@ import type {
} from '../../triggers_actions_ui/public'; } from '../../triggers_actions_ui/public';
import { registerApmAlerts } from './components/alerting/register_apm_alerts'; import { registerApmAlerts } from './components/alerting/register_apm_alerts';
import { featureCatalogueEntry } from './featureCatalogueEntry'; import { featureCatalogueEntry } from './featureCatalogueEntry';
import {
getApmEnrollmentFlyoutData,
LazyApmCustomAssetsExtension,
} from './components/fleet_integration';
export type ApmPluginSetup = ReturnType<ApmPlugin['setup']>; export type ApmPluginSetup = ReturnType<ApmPlugin['setup']>;
@ -69,6 +74,7 @@ export interface ApmPluginStartDeps {
ml?: MlPluginStart; ml?: MlPluginStart;
triggersActionsUi: TriggersAndActionsUIPublicPluginStart; triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
observability: ObservabilityPublicStart; observability: ObservabilityPublicStart;
fleet: FleetStart;
} }
export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> { export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> {
@ -303,5 +309,22 @@ export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> {
return {}; return {};
} }
public start(core: CoreStart, plugins: ApmPluginStartDeps) {} public start(core: CoreStart, plugins: ApmPluginStartDeps) {
const { fleet } = plugins;
const agentEnrollmentExtensionData = getApmEnrollmentFlyoutData();
fleet.registerExtension({
package: 'apm',
view: 'agent-enrollment-flyout',
title: agentEnrollmentExtensionData.title,
Component: agentEnrollmentExtensionData.Component,
});
fleet.registerExtension({
package: 'apm',
view: 'package-detail-assets',
Component: LazyApmCustomAssetsExtension,
});
}
} }

View file

@ -425,7 +425,7 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
[params, updatePackageInfo, agentPolicy, updateAgentPolicy, queryParamsPolicyId] [params, updatePackageInfo, agentPolicy, updateAgentPolicy, queryParamsPolicyId]
); );
const ExtensionView = useUIExtension(packagePolicy.package?.name ?? '', 'package-policy-create'); const extensionView = useUIExtension(packagePolicy.package?.name ?? '', 'package-policy-create');
const stepConfigurePackagePolicy = useMemo( const stepConfigurePackagePolicy = useMemo(
() => () =>
@ -444,7 +444,7 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
/> />
{/* Only show the out-of-box configuration step if a UI extension is NOT registered */} {/* Only show the out-of-box configuration step if a UI extension is NOT registered */}
{!ExtensionView && ( {!extensionView && (
<StepConfigurePackagePolicy <StepConfigurePackagePolicy
packageInfo={packageInfo} packageInfo={packageInfo}
showOnlyIntegration={integrationInfo?.name} showOnlyIntegration={integrationInfo?.name}
@ -456,9 +456,12 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
)} )}
{/* If an Agent Policy and a package has been selected, then show UI extension (if any) */} {/* If an Agent Policy and a package has been selected, then show UI extension (if any) */}
{ExtensionView && packagePolicy.policy_id && packagePolicy.package?.name && ( {extensionView && packagePolicy.policy_id && packagePolicy.package?.name && (
<ExtensionWrapper> <ExtensionWrapper>
<ExtensionView newPolicy={packagePolicy} onChange={handleExtensionViewOnChange} /> <extensionView.Component
newPolicy={packagePolicy}
onChange={handleExtensionViewOnChange}
/>
</ExtensionWrapper> </ExtensionWrapper>
)} )}
</> </>
@ -474,7 +477,7 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
validationResults, validationResults,
formState, formState,
integrationInfo?.name, integrationInfo?.name,
ExtensionView, extensionView,
handleExtensionViewOnChange, handleExtensionViewOnChange,
] ]
); );

View file

@ -338,7 +338,7 @@ export const EditPackagePolicyForm = memo<{
packageInfo, packageInfo,
}; };
const ExtensionView = useUIExtension(packagePolicy.package?.name ?? '', 'package-policy-edit'); const extensionView = useUIExtension(packagePolicy.package?.name ?? '', 'package-policy-edit');
const configurePackage = useMemo( const configurePackage = useMemo(
() => () =>
@ -354,7 +354,7 @@ export const EditPackagePolicyForm = memo<{
/> />
{/* Only show the out-of-box configuration step if a UI extension is NOT registered */} {/* Only show the out-of-box configuration step if a UI extension is NOT registered */}
{!ExtensionView && ( {!extensionView && (
<StepConfigurePackagePolicy <StepConfigurePackagePolicy
packageInfo={packageInfo} packageInfo={packageInfo}
packagePolicy={packagePolicy} packagePolicy={packagePolicy}
@ -364,12 +364,12 @@ export const EditPackagePolicyForm = memo<{
/> />
)} )}
{ExtensionView && {extensionView &&
packagePolicy.policy_id && packagePolicy.policy_id &&
packagePolicy.package?.name && packagePolicy.package?.name &&
originalPackagePolicy && ( originalPackagePolicy && (
<ExtensionWrapper> <ExtensionWrapper>
<ExtensionView <extensionView.Component
policy={originalPackagePolicy} policy={originalPackagePolicy}
newPolicy={packagePolicy} newPolicy={packagePolicy}
onChange={handleExtensionViewOnChange} onChange={handleExtensionViewOnChange}
@ -386,7 +386,7 @@ export const EditPackagePolicyForm = memo<{
validationResults, validationResults,
formState, formState,
originalPackagePolicy, originalPackagePolicy,
ExtensionView, extensionView,
handleExtensionViewOnChange, handleExtensionViewOnChange,
] ]
); );

View file

@ -19,7 +19,7 @@ export const DisplayedAssets: ServiceNameToAssetTypes = {
elasticsearch: Object.values(ElasticsearchAssetType), elasticsearch: Object.values(ElasticsearchAssetType),
}; };
export type DisplayedAssetType = ElasticsearchAssetType | KibanaAssetType; export type DisplayedAssetType = ElasticsearchAssetType | KibanaAssetType | 'view';
export const AssetTitleMap: Record<DisplayedAssetType, string> = { export const AssetTitleMap: Record<DisplayedAssetType, string> = {
dashboard: 'Dashboard', dashboard: 'Dashboard',
@ -36,6 +36,7 @@ export const AssetTitleMap: Record<DisplayedAssetType, string> = {
lens: 'Lens', lens: 'Lens',
security_rule: 'Security Rule', security_rule: 'Security Rule',
ml_module: 'ML Module', ml_module: 'ML Module',
view: 'Views',
}; };
export const ServiceTitleMap: Record<ServiceName, string> = { export const ServiceTitleMap: Record<ServiceName, string> = {

View file

@ -10,12 +10,17 @@ import { Redirect } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react'; import { FormattedMessage } from '@kbn/i18n/react';
import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiSpacer } from '@elastic/eui'; import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiSpacer } from '@elastic/eui';
import { Loading, Error } from '../../../../../components'; import { Loading, Error, ExtensionWrapper } from '../../../../../components';
import type { PackageInfo } from '../../../../../types'; import type { PackageInfo } from '../../../../../types';
import { InstallStatus } from '../../../../../types'; import { InstallStatus } from '../../../../../types';
import { useGetPackageInstallStatus, useLink, useStartServices } from '../../../../../hooks'; import {
useGetPackageInstallStatus,
useLink,
useStartServices,
useUIExtension,
} from '../../../../../hooks';
import type { AssetSavedObject } from './types'; import type { AssetSavedObject } from './types';
import { allowedAssetTypes } from './constants'; import { allowedAssetTypes } from './constants';
@ -27,9 +32,12 @@ interface AssetsPanelProps {
export const AssetsPage = ({ packageInfo }: AssetsPanelProps) => { export const AssetsPage = ({ packageInfo }: AssetsPanelProps) => {
const { name, version } = packageInfo; const { name, version } = packageInfo;
const pkgkey = `${name}-${version}`;
const { const {
savedObjects: { client: savedObjectsClient }, savedObjects: { client: savedObjectsClient },
} = useStartServices(); } = useStartServices();
const customAssetsExtension = useUIExtension(packageInfo.name, 'package-detail-assets');
const { getPath } = useLink(); const { getPath } = useLink();
const getPackageInstallStatus = useGetPackageInstallStatus(); const getPackageInstallStatus = useGetPackageInstallStatus();
@ -76,13 +84,7 @@ export const AssetsPage = ({ packageInfo }: AssetsPanelProps) => {
// if they arrive at this page and the package is not installed, send them to overview // 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 // this happens if they arrive with a direct url or they uninstall while on this tab
if (packageInstallStatus.status !== InstallStatus.installed) { if (packageInstallStatus.status !== InstallStatus.installed) {
return ( return <Redirect to={getPath('integration_details_overview', { pkgkey })} />;
<Redirect
to={getPath('integration_details_overview', {
pkgkey: `${name}-${version}`,
})}
/>
);
} }
let content: JSX.Element | Array<JSX.Element | null>; let content: JSX.Element | Array<JSX.Element | null>;
@ -102,6 +104,15 @@ export const AssetsPage = ({ packageInfo }: AssetsPanelProps) => {
/> />
); );
} else if (assetSavedObjects === undefined) { } else if (assetSavedObjects === undefined) {
if (customAssetsExtension) {
// If a UI extension for custom asset entries is defined, render the custom component here depisite
// there being no saved objects found
content = (
<ExtensionWrapper>
<customAssetsExtension.Component />
</ExtensionWrapper>
);
} else {
content = ( content = (
<EuiTitle> <EuiTitle>
<h2> <h2>
@ -112,8 +123,10 @@ export const AssetsPage = ({ packageInfo }: AssetsPanelProps) => {
</h2> </h2>
</EuiTitle> </EuiTitle>
); );
}
} else { } else {
content = allowedAssetTypes.map((assetType) => { content = [
...allowedAssetTypes.map((assetType) => {
const sectionAssetSavedObjects = assetSavedObjects.filter((so) => so.type === assetType); const sectionAssetSavedObjects = assetSavedObjects.filter((so) => so.type === assetType);
if (!sectionAssetSavedObjects.length) { if (!sectionAssetSavedObjects.length) {
@ -126,7 +139,14 @@ export const AssetsPage = ({ packageInfo }: AssetsPanelProps) => {
<EuiSpacer size="l" /> <EuiSpacer size="l" />
</> </>
); );
}); }),
// 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,
];
} }
return ( return (

View file

@ -55,6 +55,11 @@ export const AssetsAccordion: FunctionComponent<Props> = ({ savedObjects, type }
<EuiSpacer size="m" /> <EuiSpacer size="m" />
<EuiSplitPanel.Outer hasBorder hasShadow={false}> <EuiSplitPanel.Outer hasBorder hasShadow={false}>
{savedObjects.map(({ id, attributes: { title, description } }, idx) => { {savedObjects.map(({ id, attributes: { title, description } }, idx) => {
// Ignore custom asset views
if (type === 'view') {
return;
}
const pathToObjectInApp = getHrefToObjectInKibanaApp({ const pathToObjectInApp = getHrefToObjectInKibanaApp({
http, http,
id, id,

View file

@ -17,4 +17,4 @@ export type AllowedAssetTypes = [
KibanaAssetType.visualization KibanaAssetType.visualization
]; ];
export type AllowedAssetType = AllowedAssetTypes[number]; export type AllowedAssetType = AllowedAssetTypes[number] | 'view';

View file

@ -18,16 +18,16 @@ interface Props {
} }
export const CustomViewPage: React.FC<Props> = memo(({ packageInfo }) => { export const CustomViewPage: React.FC<Props> = memo(({ packageInfo }) => {
const CustomView = useUIExtension(packageInfo.name, 'package-detail-custom'); const customViewExtension = useUIExtension(packageInfo.name, 'package-detail-custom');
const { getPath } = useLink(); const { getPath } = useLink();
const pkgkey = useMemo(() => pkgKeyFromPackageInfo(packageInfo), [packageInfo]); const pkgkey = useMemo(() => pkgKeyFromPackageInfo(packageInfo), [packageInfo]);
return CustomView ? ( return customViewExtension ? (
<EuiFlexGroup alignItems="flexStart"> <EuiFlexGroup alignItems="flexStart">
<EuiFlexItem grow={1} /> <EuiFlexItem grow={1} />
<EuiFlexItem grow={6}> <EuiFlexItem grow={6}>
<ExtensionWrapper> <ExtensionWrapper>
<CustomView pkgkey={pkgkey} packageInfo={packageInfo} /> <customViewExtension.Component pkgkey={pkgkey} packageInfo={packageInfo} />
</ExtensionWrapper> </ExtensionWrapper>
</EuiFlexItem> </EuiFlexItem>
</EuiFlexGroup> </EuiFlexGroup>

View file

@ -113,7 +113,7 @@ describe('when on integration detail', () => {
}); });
}); });
describe('and a custom UI extension is registered', () => { describe('and a custom tab UI extension is registered', () => {
// Because React Lazy components are loaded async (Promise), we setup this "watcher" Promise // Because React Lazy components are loaded async (Promise), we setup this "watcher" Promise
// that is `resolved` once the lazy components actually renders. // that is `resolved` once the lazy components actually renders.
let lazyComponentWasRendered: Promise<void>; let lazyComponentWasRendered: Promise<void>;
@ -136,7 +136,7 @@ describe('when on integration detail', () => {
testRenderer.startInterface.registerExtension({ testRenderer.startInterface.registerExtension({
package: 'nginx', package: 'nginx',
view: 'package-detail-custom', view: 'package-detail-custom',
component: CustomComponent, Component: CustomComponent,
}); });
render(); render();
@ -162,6 +162,53 @@ describe('when on integration detail', () => {
}); });
}); });
describe('and a custom assets UI extension is registered', () => {
let lazyComponentWasRendered: Promise<void>;
beforeEach(() => {
let setWasRendered: () => void;
lazyComponentWasRendered = new Promise((resolve) => {
setWasRendered = resolve;
});
const CustomComponent = lazy(async () => {
return {
default: memo(() => {
setWasRendered();
return <div data-test-subj="custom-hello">hello</div>;
}),
};
});
testRenderer.startInterface.registerExtension({
package: 'nginx',
view: 'package-detail-assets',
Component: CustomComponent,
});
render();
});
afterEach(() => {
// @ts-ignore
lazyComponentWasRendered = undefined;
});
it('should display "assets" tab in navigation', () => {
expect(renderResult.getByTestId('tab-assets'));
});
it('should display custom assets when tab is clicked', async () => {
act(() => {
testRenderer.history.push(
pagePathGetters.integration_details_assets({ pkgkey: 'nginx-0.3.7' })[1]
);
});
await lazyComponentWasRendered;
expect(renderResult.getByTestId('custom-hello'));
});
});
describe('and the Add integration button is clicked', () => { describe('and the Add integration button is clicked', () => {
beforeEach(() => render()); beforeEach(() => render());

View file

@ -101,6 +101,8 @@ export function Detail() {
const setPackageInstallStatus = useSetPackageInstallStatus(); const setPackageInstallStatus = useSetPackageInstallStatus();
const getPackageInstallStatus = useGetPackageInstallStatus(); const getPackageInstallStatus = useGetPackageInstallStatus();
const CustomAssets = useUIExtension(packageInfo?.name ?? '', 'package-detail-assets');
const packageInstallStatus = useMemo(() => { const packageInstallStatus = useMemo(() => {
if (packageInfo === null || !packageInfo.name) { if (packageInfo === null || !packageInfo.name) {
return undefined; return undefined;
@ -418,7 +420,7 @@ export function Detail() {
}); });
} }
if (packageInstallStatus === InstallStatus.installed && packageInfo.assets) { if (packageInstallStatus === InstallStatus.installed && (packageInfo.assets || CustomAssets)) {
tabs.push({ tabs.push({
id: 'assets', id: 'assets',
name: ( name: (
@ -471,7 +473,7 @@ export function Detail() {
} }
return tabs; return tabs;
}, [packageInfo, panel, getHref, integration, packageInstallStatus, showCustomTab]); }, [packageInfo, panel, getHref, integration, packageInstallStatus, showCustomTab, CustomAssets]);
return ( return (
<WithHeaderLayout <WithHeaderLayout

View file

@ -7,7 +7,11 @@
import { stringify, parse } from 'query-string'; import { stringify, parse } from 'query-string';
import React, { memo, useCallback, useMemo, useState } from 'react'; import React, { memo, useCallback, useMemo, useState } from 'react';
import { Redirect, useLocation, useHistory } from 'react-router-dom'; import { Redirect, useLocation, useHistory } from 'react-router-dom';
import type { CriteriaWithPagination, EuiTableFieldDataColumnType } from '@elastic/eui'; import type {
CriteriaWithPagination,
EuiStepProps,
EuiTableFieldDataColumnType,
} from '@elastic/eui';
import { import {
EuiButtonIcon, EuiButtonIcon,
EuiBasicTable, EuiBasicTable,
@ -29,6 +33,7 @@ import {
useUrlPagination, useUrlPagination,
useGetPackageInstallStatus, useGetPackageInstallStatus,
AgentPolicyRefreshContext, AgentPolicyRefreshContext,
useUIExtension,
} from '../../../../../hooks'; } from '../../../../../hooks';
import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants';
import { import {
@ -88,6 +93,8 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps
kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: ${name}`, kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: ${name}`,
}); });
const agentEnrollmentFlyoutExtension = useUIExtension(name, 'agent-enrollment-flyout');
const handleTableOnChange = useCallback( const handleTableOnChange = useCallback(
({ page }: CriteriaWithPagination<PackagePolicyAndAgentPolicy>) => { ({ page }: CriteriaWithPagination<PackagePolicyAndAgentPolicy>) => {
setPagination({ setPagination({
@ -98,8 +105,19 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps
[setPagination] [setPagination]
); );
const renderViewDataStepContent = useCallback( const viewDataStep = useMemo<EuiStepProps>(() => {
() => ( if (agentEnrollmentFlyoutExtension) {
return {
title: agentEnrollmentFlyoutExtension.title,
children: <agentEnrollmentFlyoutExtension.Component />,
};
}
return {
title: i18n.translate('xpack.fleet.agentEnrollment.stepViewDataTitle', {
defaultMessage: 'View your data',
}),
children: (
<> <>
<EuiText> <EuiText>
<FormattedMessage <FormattedMessage
@ -125,8 +143,8 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps
</EuiButton> </EuiButton>
</> </>
), ),
[name, version, getHref] };
); }, [name, version, getHref, agentEnrollmentFlyoutExtension]);
const columns: Array<EuiTableFieldDataColumnType<PackagePolicyAndAgentPolicy>> = useMemo( const columns: Array<EuiTableFieldDataColumnType<PackagePolicyAndAgentPolicy>> = useMemo(
() => [ () => [
@ -230,13 +248,13 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps
<PackagePolicyActionsMenu <PackagePolicyActionsMenu
agentPolicy={agentPolicy} agentPolicy={agentPolicy}
packagePolicy={packagePolicy} packagePolicy={packagePolicy}
viewDataStepContent={renderViewDataStepContent()} viewDataStep={viewDataStep}
/> />
); );
}, },
}, },
], ],
[renderViewDataStepContent] [viewDataStep]
); );
const noItemsMessage = useMemo(() => { const noItemsMessage = useMemo(() => {
@ -292,7 +310,7 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps
data?.items.find(({ agentPolicy }) => agentPolicy.id === flyoutOpenForPolicyId) data?.items.find(({ agentPolicy }) => agentPolicy.id === flyoutOpenForPolicyId)
?.agentPolicy ?.agentPolicy
} }
viewDataStepContent={renderViewDataStepContent()} viewDataStep={viewDataStep}
/> />
)} )}
</AgentPolicyRefreshContext.Provider> </AgentPolicyRefreshContext.Provider>

View file

@ -21,7 +21,7 @@ import { FleetStatusProvider, ConfigContext } from '../../hooks';
import { useFleetServerInstructions } from '../../applications/fleet/sections/agents/agent_requirements_page/components'; import { useFleetServerInstructions } from '../../applications/fleet/sections/agents/agent_requirements_page/components';
import { AgentEnrollmentKeySelectionStep, AgentPolicySelectionStep, ViewDataStep } from './steps'; import { AgentEnrollmentKeySelectionStep, AgentPolicySelectionStep } from './steps';
import type { Props } from '.'; import type { Props } from '.';
import { AgentEnrollmentFlyout } from '.'; import { AgentEnrollmentFlyout } from '.';
@ -129,24 +129,26 @@ describe('<AgentEnrollmentFlyout />', () => {
}); });
}); });
describe('"View data" extension point', () => { // Skipped due to implementation details in the step components. See https://github.com/elastic/kibana/issues/103894
it('calls the "View data" step when UI extension is provided', async () => { describe.skip('"View data" extension point', () => {
it('shows the "View data" step when UI extension is provided', async () => {
jest.clearAllMocks(); jest.clearAllMocks();
await act(async () => { await act(async () => {
testBed = await setup({ testBed = await setup({
agentPolicies: [], agentPolicies: [],
onClose: jest.fn(), onClose: jest.fn(),
viewDataStepContent: <div />, viewDataStep: { title: 'View Data', children: <div /> },
}); });
testBed.component.update(); testBed.component.update();
}); });
const { exists, actions } = testBed; const { exists, actions } = testBed;
expect(exists('agentEnrollmentFlyout')).toBe(true); expect(exists('agentEnrollmentFlyout')).toBe(true);
expect(ViewDataStep).toHaveBeenCalled(); expect(exists('view-data-step')).toBe(true);
jest.clearAllMocks(); jest.clearAllMocks();
actions.goToStandaloneTab(); actions.goToStandaloneTab();
expect(ViewDataStep).not.toHaveBeenCalled(); expect(exists('agentEnrollmentFlyout')).toBe(true);
expect(exists('view-data-step')).toBe(false);
}); });
it('does not call the "View data" step when UI extension is not provided', async () => { it('does not call the "View data" step when UI extension is not provided', async () => {
@ -155,17 +157,17 @@ describe('<AgentEnrollmentFlyout />', () => {
testBed = await setup({ testBed = await setup({
agentPolicies: [], agentPolicies: [],
onClose: jest.fn(), onClose: jest.fn(),
viewDataStepContent: undefined, viewDataStep: undefined,
}); });
testBed.component.update(); testBed.component.update();
}); });
const { exists, actions } = testBed; const { exists, actions } = testBed;
expect(exists('agentEnrollmentFlyout')).toBe(true); expect(exists('agentEnrollmentFlyout')).toBe(true);
expect(ViewDataStep).not.toHaveBeenCalled(); expect(exists('view-data-step')).toBe(false);
jest.clearAllMocks(); jest.clearAllMocks();
actions.goToStandaloneTab(); actions.goToStandaloneTab();
expect(ViewDataStep).not.toHaveBeenCalled(); expect(exists('view-data-step')).toBe(false);
}); });
}); });
}); });

View file

@ -45,7 +45,7 @@ export const AgentEnrollmentFlyout: React.FunctionComponent<Props> = ({
onClose, onClose,
agentPolicy, agentPolicy,
agentPolicies, agentPolicies,
viewDataStepContent, viewDataStep,
defaultMode = 'managed', defaultMode = 'managed',
}) => { }) => {
const [mode, setMode] = useState<FlyoutMode>(defaultMode); const [mode, setMode] = useState<FlyoutMode>(defaultMode);
@ -119,14 +119,10 @@ export const AgentEnrollmentFlyout: React.FunctionComponent<Props> = ({
<ManagedInstructions <ManagedInstructions
agentPolicy={agentPolicy} agentPolicy={agentPolicy}
agentPolicies={agentPolicies} agentPolicies={agentPolicies}
viewDataStepContent={viewDataStepContent} viewDataStep={viewDataStep}
/> />
) : ( ) : (
<StandaloneInstructions <StandaloneInstructions agentPolicy={agentPolicy} agentPolicies={agentPolicies} />
agentPolicy={agentPolicy}
agentPolicies={agentPolicies}
viewDataStepContent={viewDataStepContent}
/>
)} )}
</EuiFlyoutBody> </EuiFlyoutBody>
<EuiFlyoutFooter> <EuiFlyoutFooter>

View file

@ -23,12 +23,7 @@ import {
} from '../../applications/fleet/sections/agents/agent_requirements_page/components'; } from '../../applications/fleet/sections/agents/agent_requirements_page/components';
import { FleetServerRequirementPage } from '../../applications/fleet/sections/agents/agent_requirements_page'; import { FleetServerRequirementPage } from '../../applications/fleet/sections/agents/agent_requirements_page';
import { import { DownloadStep, AgentPolicySelectionStep, AgentEnrollmentKeySelectionStep } from './steps';
DownloadStep,
AgentPolicySelectionStep,
AgentEnrollmentKeySelectionStep,
ViewDataStep,
} from './steps';
import type { BaseProps } from './types'; import type { BaseProps } from './types';
type Props = BaseProps; type Props = BaseProps;
@ -61,7 +56,7 @@ const FleetServerMissingRequirements = () => {
}; };
export const ManagedInstructions = React.memo<Props>( export const ManagedInstructions = React.memo<Props>(
({ agentPolicy, agentPolicies, viewDataStepContent }) => { ({ agentPolicy, agentPolicies, viewDataStep }) => {
const fleetStatus = useFleetStatus(); const fleetStatus = useFleetStatus();
const [selectedApiKeyId, setSelectedAPIKeyId] = useState<string | undefined>(); const [selectedApiKeyId, setSelectedAPIKeyId] = useState<string | undefined>();
@ -118,8 +113,8 @@ export const ManagedInstructions = React.memo<Props>(
}); });
} }
if (viewDataStepContent) { if (viewDataStep) {
baseSteps.push(ViewDataStep(viewDataStepContent)); baseSteps.push({ 'data-test-subj': 'view-data-step', ...viewDataStep });
} }
return baseSteps; return baseSteps;
@ -127,12 +122,12 @@ export const ManagedInstructions = React.memo<Props>(
agentPolicy, agentPolicy,
selectedApiKeyId, selectedApiKeyId,
setSelectedAPIKeyId, setSelectedAPIKeyId,
viewDataStepContent,
agentPolicies, agentPolicies,
apiKey.data, apiKey.data,
fleetServerSteps, fleetServerSteps,
isFleetServerPolicySelected, isFleetServerPolicySelected,
settings.data?.item?.fleet_server_hosts, settings.data?.item?.fleet_server_hosts,
viewDataStep,
]); ]);
return ( return (

View file

@ -144,16 +144,3 @@ export const AgentEnrollmentKeySelectionStep = ({
), ),
}; };
}; };
/**
* Send users to assets installed by the package in Kibana so they can
* view their data.
*/
export const ViewDataStep = (content: JSX.Element) => {
return {
title: i18n.translate('xpack.fleet.agentEnrollment.stepViewDataTitle', {
defaultMessage: 'View your data',
}),
children: content,
};
};

View file

@ -5,6 +5,8 @@
* 2.0. * 2.0.
*/ */
import type { EuiStepProps } from '@elastic/eui';
import type { AgentPolicy } from '../../types'; import type { AgentPolicy } from '../../types';
export interface BaseProps { export interface BaseProps {
@ -24,5 +26,5 @@ export interface BaseProps {
* There is a step in the agent enrollment process that allows users to see the data from an integration represented in the UI * There is a step in the agent enrollment process that allows users to see the data from an integration represented in the UI
* in some way. This is an area for consumers to render a button and text explaining how data can be viewed. * in some way. This is an area for consumers to render a button and text explaining how data can be viewed.
*/ */
viewDataStepContent?: JSX.Element; viewDataStep?: EuiStepProps;
} }

View file

@ -0,0 +1,86 @@
/*
* 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 type { FunctionComponent } from 'react';
import {
EuiAccordion,
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiNotificationBadge,
EuiSpacer,
EuiSplitPanel,
EuiLink,
EuiHorizontalRule,
} from '@elastic/eui';
import { AssetTitleMap } from '../applications/integrations/sections/epm/constants';
import { useStartServices } from '../hooks';
import { RedirectAppLinks } from '../../../../../src/plugins/kibana_react/public';
export interface CustomAssetsAccordionProps {
views: Array<{
name: string;
url: string;
description: string;
}>;
initialIsOpen?: boolean;
}
export const CustomAssetsAccordion: FunctionComponent<CustomAssetsAccordionProps> = ({
views,
initialIsOpen = false,
}) => {
const { application } = useStartServices();
return (
<EuiAccordion
initialIsOpen={initialIsOpen}
buttonContent={
<EuiFlexGroup justifyContent="center" alignItems="center" gutterSize="s" responsive={false}>
<EuiFlexItem grow={false}>
<EuiText size="m">
<h3>{AssetTitleMap.view}</h3>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiNotificationBadge color="subdued" size="m">
<h3>{views.length}</h3>
</EuiNotificationBadge>
</EuiFlexItem>
</EuiFlexGroup>
}
id="custom-assets"
>
<>
<EuiSpacer size="m" />
<EuiSplitPanel.Outer hasBorder hasShadow={false}>
{views.map((view, index) => (
<>
<EuiSplitPanel.Inner grow={false} key={index}>
<EuiText size="m">
<p>
<RedirectAppLinks application={application}>
<EuiLink href={view.url}>{view.name}</EuiLink>
</RedirectAppLinks>
</p>
</EuiText>
<EuiSpacer size="s" />
<EuiText size="s" color="subdued">
<p>{view.description}</p>
</EuiText>
</EuiSplitPanel.Inner>
{index + 1 < views.length && <EuiHorizontalRule margin="none" />}
</>
))}
</EuiSplitPanel.Outer>
</>
</EuiAccordion>
);
};

View file

@ -7,6 +7,7 @@
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { EuiContextMenuItem, EuiPortal } from '@elastic/eui'; import { EuiContextMenuItem, EuiPortal } from '@elastic/eui';
import type { EuiStepProps } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react'; import { FormattedMessage } from '@kbn/i18n/react';
import type { AgentPolicy, PackagePolicy } from '../types'; import type { AgentPolicy, PackagePolicy } from '../types';
@ -21,8 +22,8 @@ import { PackagePolicyDeleteProvider } from './package_policy_delete_provider';
export const PackagePolicyActionsMenu: React.FunctionComponent<{ export const PackagePolicyActionsMenu: React.FunctionComponent<{
agentPolicy: AgentPolicy; agentPolicy: AgentPolicy;
packagePolicy: PackagePolicy; packagePolicy: PackagePolicy;
viewDataStepContent?: JSX.Element; viewDataStep?: EuiStepProps;
}> = ({ agentPolicy, packagePolicy, viewDataStepContent }) => { }> = ({ agentPolicy, packagePolicy, viewDataStep }) => {
const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState(false); const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState(false);
const { getHref } = useLink(); const { getHref } = useLink();
const hasWriteCapabilities = useCapabilities().write; const hasWriteCapabilities = useCapabilities().write;
@ -106,7 +107,7 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{
<EuiPortal> <EuiPortal>
<AgentEnrollmentFlyout <AgentEnrollmentFlyout
agentPolicy={agentPolicy} agentPolicy={agentPolicy}
viewDataStepContent={viewDataStepContent} viewDataStep={viewDataStep}
onClose={onEnrollmentFlyoutClose} onClose={onEnrollmentFlyoutClose}
/> />
</EuiPortal> </EuiPortal>

View file

@ -24,3 +24,5 @@ export {
export * from './page_paths'; export * from './page_paths';
export const INDEX_NAME = '.kibana'; export const INDEX_NAME = '.kibana';
export const CUSTOM_LOGS_INTEGRATION_NAME = 'log';

View file

@ -0,0 +1,31 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { CustomAssetsAccordion } from './components/custom_assets_accordion';
import type { CustomAssetsAccordionProps } from './components/custom_assets_accordion';
import { useStartServices } from './hooks';
import type { PackageAssetsComponent } from './types';
export const CustomLogsAssetsExtension: PackageAssetsComponent = () => {
const { http } = useStartServices();
const logStreamUrl = http.basePath.prepend('/app/logs/stream');
const views: CustomAssetsAccordionProps['views'] = [
{
name: i18n.translate('xpack.fleet.assets.customLogs.name', { defaultMessage: 'Logs' }),
url: logStreamUrl,
description: i18n.translate('xpack.fleet.assets.customLogs.description', {
defaultMessage: 'View Custom logs data in Logs app',
}),
},
];
return <CustomAssetsAccordion views={views} initialIsOpen />;
};

View file

@ -20,7 +20,7 @@ type NarrowExtensionPoint<V extends UIExtensionPoint['view'], A = UIExtensionPoi
export const useUIExtension = <V extends UIExtensionPoint['view'] = UIExtensionPoint['view']>( export const useUIExtension = <V extends UIExtensionPoint['view'] = UIExtensionPoint['view']>(
packageName: UIExtensionPoint['package'], packageName: UIExtensionPoint['package'],
view: V view: V
): NarrowExtensionPoint<V>['component'] | undefined => { ): NarrowExtensionPoint<V> | undefined => {
const registeredExtensions = useContext(UIExtensionsContext); const registeredExtensions = useContext(UIExtensionsContext);
if (!registeredExtensions) { if (!registeredExtensions) {
@ -32,6 +32,6 @@ export const useUIExtension = <V extends UIExtensionPoint['view'] = UIExtensionP
if (extension) { if (extension) {
// FIXME:PT Revisit ignore below and see if TS error can be addressed // FIXME:PT Revisit ignore below and see if TS error can be addressed
// @ts-ignore // @ts-ignore
return extension.component; return extension;
} }
}; };

View file

@ -21,3 +21,7 @@ export * from './types/ui_extensions';
export { pagePathGetters } from './constants'; export { pagePathGetters } from './constants';
export { pkgKeyFromPackageInfo } from './services'; export { pkgKeyFromPackageInfo } from './services';
export {
CustomAssetsAccordion,
CustomAssetsAccordionProps,
} from './components/custom_assets_accordion';

View file

@ -0,0 +1,17 @@
/*
* 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 { lazy } from 'react';
import type { PackageAssetsComponent } from './types';
import { CustomLogsAssetsExtension } from './custom_logs_assets_extension';
export const LazyCustomLogsAssetsExtension = lazy<PackageAssetsComponent>(async () => {
return {
default: CustomLogsAssetsExtension,
};
});

View file

@ -32,7 +32,7 @@ import type { CheckPermissionsResponse, PostIngestSetupResponse } from '../commo
import type { FleetConfigType } from '../common/types'; import type { FleetConfigType } from '../common/types';
import { FLEET_BASE_PATH } from './constants'; import { CUSTOM_LOGS_INTEGRATION_NAME, FLEET_BASE_PATH } from './constants';
import { licenseService } from './hooks'; import { licenseService } from './hooks';
import { setHttpClient } from './hooks/use_request'; import { setHttpClient } from './hooks/use_request';
import { createPackageSearchProvider } from './search_provider'; import { createPackageSearchProvider } from './search_provider';
@ -43,6 +43,7 @@ import {
} from './components/home_integration'; } from './components/home_integration';
import { createExtensionRegistrationCallback } from './services/ui_extensions'; import { createExtensionRegistrationCallback } from './services/ui_extensions';
import type { UIExtensionRegistrationCallback, UIExtensionsStorage } from './types'; import type { UIExtensionRegistrationCallback, UIExtensionsStorage } from './types';
import { LazyCustomLogsAssetsExtension } from './lazy_custom_logs_assets_extension';
export { FleetConfigType } from '../common/types'; export { FleetConfigType } from '../common/types';
@ -204,6 +205,13 @@ export class FleetPlugin implements Plugin<FleetSetup, FleetStart, FleetSetupDep
public start(core: CoreStart): FleetStart { public start(core: CoreStart): FleetStart {
let successPromise: ReturnType<FleetStart['isInitialized']>; let successPromise: ReturnType<FleetStart['isInitialized']>;
const registerExtension = createExtensionRegistrationCallback(this.extensions);
registerExtension({
package: CUSTOM_LOGS_INTEGRATION_NAME,
view: 'package-detail-assets',
Component: LazyCustomLogsAssetsExtension,
});
return { return {
isInitialized: () => { isInitialized: () => {
@ -229,8 +237,7 @@ export class FleetPlugin implements Plugin<FleetSetup, FleetStart, FleetSetupDep
return successPromise; return successPromise;
}, },
registerExtension,
registerExtension: createExtensionRegistrationCallback(this.extensions),
}; };
} }

View file

@ -36,13 +36,13 @@ describe('UI Extension services', () => {
register({ register({
view: 'package-policy-edit', view: 'package-policy-edit',
package: 'endpoint', package: 'endpoint',
component: LazyCustomView, Component: LazyCustomView,
}); });
expect(storage.endpoint['package-policy-edit']).toEqual({ expect(storage.endpoint['package-policy-edit']).toEqual({
view: 'package-policy-edit', view: 'package-policy-edit',
package: 'endpoint', package: 'endpoint',
component: LazyCustomView, Component: LazyCustomView,
}); });
}); });
@ -57,21 +57,21 @@ describe('UI Extension services', () => {
register({ register({
view: 'package-policy-edit', view: 'package-policy-edit',
package: 'endpoint', package: 'endpoint',
component: LazyCustomView, Component: LazyCustomView,
}); });
expect(() => { expect(() => {
register({ register({
view: 'package-policy-edit', view: 'package-policy-edit',
package: 'endpoint', package: 'endpoint',
component: LazyCustomView2, Component: LazyCustomView2,
}); });
}).toThrow(); }).toThrow();
expect(storage.endpoint['package-policy-edit']).toEqual({ expect(storage.endpoint['package-policy-edit']).toEqual({
view: 'package-policy-edit', view: 'package-policy-edit',
package: 'endpoint', package: 'endpoint',
component: LazyCustomView, Component: LazyCustomView,
}); });
}); });
}); });

View file

@ -5,6 +5,7 @@
* 2.0. * 2.0.
*/ */
import type { EuiStepProps } from '@elastic/eui';
import type { ComponentType, LazyExoticComponent } from 'react'; import type { ComponentType, LazyExoticComponent } from 'react';
import type { NewPackagePolicy, PackageInfo, PackagePolicy } from './index'; import type { NewPackagePolicy, PackageInfo, PackagePolicy } from './index';
@ -48,7 +49,7 @@ export interface PackagePolicyEditExtensionComponentProps {
export interface PackagePolicyEditExtension { export interface PackagePolicyEditExtension {
package: string; package: string;
view: 'package-policy-edit'; view: 'package-policy-edit';
component: LazyExoticComponent<PackagePolicyEditExtensionComponent>; Component: LazyExoticComponent<PackagePolicyEditExtensionComponent>;
} }
/** /**
@ -76,7 +77,7 @@ export interface PackagePolicyCreateExtensionComponentProps {
export interface PackagePolicyCreateExtension { export interface PackagePolicyCreateExtension {
package: string; package: string;
view: 'package-policy-create'; view: 'package-policy-create';
component: LazyExoticComponent<PackagePolicyCreateExtensionComponent>; Component: LazyExoticComponent<PackagePolicyCreateExtensionComponent>;
} }
/** /**
@ -94,11 +95,32 @@ export interface PackageCustomExtensionComponentProps {
export interface PackageCustomExtension { export interface PackageCustomExtension {
package: string; package: string;
view: 'package-detail-custom'; view: 'package-detail-custom';
component: LazyExoticComponent<PackageCustomExtensionComponent>; Component: LazyExoticComponent<PackageCustomExtensionComponent>;
}
/**
* UI Component Extension for displaying custom views under the Assets tab for a given Integration
*/
export type PackageAssetsComponent = ComponentType<{}>;
/** Extension point registration contract for Integration details Assets view */
export interface PackageAssetsExtension {
package: string;
view: 'package-detail-assets';
Component: LazyExoticComponent<PackageAssetsComponent>;
}
export interface AgentEnrollmentFlyoutFinalStepExtension {
package: string;
view: 'agent-enrollment-flyout';
title: EuiStepProps['title'];
Component: ComponentType<{}>;
} }
/** Fleet UI Extension Point */ /** Fleet UI Extension Point */
export type UIExtensionPoint = export type UIExtensionPoint =
| PackagePolicyEditExtension | PackagePolicyEditExtension
| PackageCustomExtension | PackageCustomExtension
| PackagePolicyCreateExtension; | PackagePolicyCreateExtension
| PackageAssetsExtension
| AgentEnrollmentFlyoutFinalStepExtension;

View file

@ -58,7 +58,7 @@ export function toggleOsqueryPlugin(
registerExtension({ registerExtension({
package: OSQUERY_INTEGRATION_NAME, package: OSQUERY_INTEGRATION_NAME,
view: 'package-detail-custom', view: 'package-detail-custom',
component: LazyOsqueryManagedCustomButtonExtension, Component: LazyOsqueryManagedCustomButtonExtension,
}); });
} }
@ -146,13 +146,13 @@ export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginSt
registerExtension({ registerExtension({
package: OSQUERY_INTEGRATION_NAME, package: OSQUERY_INTEGRATION_NAME,
view: 'package-policy-create', view: 'package-policy-create',
component: LazyOsqueryManagedPolicyCreateImportExtension, Component: LazyOsqueryManagedPolicyCreateImportExtension,
}); });
registerExtension({ registerExtension({
package: OSQUERY_INTEGRATION_NAME, package: OSQUERY_INTEGRATION_NAME,
view: 'package-policy-edit', view: 'package-policy-edit',
component: LazyOsqueryManagedPolicyEditExtension, Component: LazyOsqueryManagedPolicyEditExtension,
}); });
} }
} else { } else {

View file

@ -0,0 +1,34 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { useKibana } from '../../../../../common/lib/kibana';
import { APP_PATH } from '../../../../../../common/constants';
import {
CustomAssetsAccordionProps,
CustomAssetsAccordion,
PackageAssetsComponent,
} from '../../../../../../../fleet/public';
export const EndpointCustomAssetsExtension: PackageAssetsComponent = () => {
const { http } = useKibana().services;
const views: CustomAssetsAccordionProps['views'] = [
{
name: i18n.translate('xpack.securitySolution.fleetIntegration.assets.name', {
defaultMessage: 'Hosts',
}),
url: http.basePath.prepend(`${APP_PATH}/administration/endpoints`),
description: i18n.translate('xpack.securitySolution.fleetIntegration.assets.description', {
defaultMessage: 'View endpoints in Security app',
}),
},
];
return <CustomAssetsAccordion views={views} initialIsOpen />;
};

View file

@ -0,0 +1,16 @@
/*
* 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 { lazy } from 'react';
export const LazyEndpointCustomAssetsExtension = lazy(async () => {
const { EndpointCustomAssetsExtension } = await import('./endpoint_custom_assets_extension');
return {
default: EndpointCustomAssetsExtension,
};
});

View file

@ -60,6 +60,7 @@ import { LazyEndpointPolicyCreateExtension } from './management/pages/policy/vie
import { getLazyEndpointPackageCustomExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_package_custom_extension'; import { getLazyEndpointPackageCustomExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_package_custom_extension';
import { parseExperimentalConfigValue } from '../common/experimental_features'; import { parseExperimentalConfigValue } from '../common/experimental_features';
import type { TimelineState } from '../../timelines/public'; import type { TimelineState } from '../../timelines/public';
import { LazyEndpointCustomAssetsExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_custom_assets_extension';
export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, StartPlugins> { export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, StartPlugins> {
private kibanaVersion: string; private kibanaVersion: string;
@ -199,19 +200,25 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
registerExtension({ registerExtension({
package: 'endpoint', package: 'endpoint',
view: 'package-policy-edit', view: 'package-policy-edit',
component: getLazyEndpointPolicyEditExtension(core, plugins), Component: getLazyEndpointPolicyEditExtension(core, plugins),
}); });
registerExtension({ registerExtension({
package: 'endpoint', package: 'endpoint',
view: 'package-policy-create', view: 'package-policy-create',
component: LazyEndpointPolicyCreateExtension, Component: LazyEndpointPolicyCreateExtension,
}); });
registerExtension({ registerExtension({
package: 'endpoint', package: 'endpoint',
view: 'package-detail-custom', view: 'package-detail-custom',
component: getLazyEndpointPackageCustomExtension(core, plugins), Component: getLazyEndpointPackageCustomExtension(core, plugins),
});
registerExtension({
package: 'endpoint',
view: 'package-detail-assets',
Component: LazyEndpointCustomAssetsExtension,
}); });
} }
licenseService.start(plugins.licensing.license$); licenseService.start(plugins.licensing.license$);

View file

@ -15,7 +15,7 @@
"server": true, "server": true,
"ui": true, "ui": true,
"version": "8.0.0", "version": "8.0.0",
"requiredBundles": ["observability", "kibanaReact", "kibanaUtils", "home", "data", "ml"], "requiredBundles": ["observability", "kibanaReact", "kibanaUtils", "home", "data", "ml", "fleet"],
"owner": { "owner": {
"name": "Uptime", "name": "Uptime",
"githubTeam": "uptime" "githubTeam": "uptime"

View file

@ -42,6 +42,7 @@ import {
LazySyntheticsPolicyCreateExtension, LazySyntheticsPolicyCreateExtension,
LazySyntheticsPolicyEditExtension, LazySyntheticsPolicyEditExtension,
} from '../components/fleet_package'; } from '../components/fleet_package';
import { LazySyntheticsCustomAssetsExtension } from '../components/fleet_package/lazy_synthetics_custom_assets_extension';
export interface ClientPluginsSetup { export interface ClientPluginsSetup {
data: DataPublicPluginSetup; data: DataPublicPluginSetup;
@ -196,13 +197,19 @@ export class UptimePlugin
registerExtension({ registerExtension({
package: 'synthetics', package: 'synthetics',
view: 'package-policy-create', view: 'package-policy-create',
component: LazySyntheticsPolicyCreateExtension, Component: LazySyntheticsPolicyCreateExtension,
}); });
registerExtension({ registerExtension({
package: 'synthetics', package: 'synthetics',
view: 'package-policy-edit', view: 'package-policy-edit',
component: LazySyntheticsPolicyEditExtension, Component: LazySyntheticsPolicyEditExtension,
});
registerExtension({
package: 'synthetics',
view: 'package-detail-assets',
Component: LazySyntheticsCustomAssetsExtension,
}); });
} }
} }

View file

@ -0,0 +1,16 @@
/*
* 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 { lazy } from 'react';
export const LazySyntheticsCustomAssetsExtension = lazy(async () => {
const { SyntheticsCustomAssetsExtension } = await import('./synthetics_custom_assets_extension');
return {
default: SyntheticsCustomAssetsExtension,
};
});

View file

@ -0,0 +1,35 @@
/*
* 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 { i18n } from '@kbn/i18n';
import {
PackageAssetsComponent,
CustomAssetsAccordionProps,
CustomAssetsAccordion,
} from '../../../../fleet/public';
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
import { ClientPluginsStart } from '../../apps/plugin';
import { PLUGIN } from '../../../common/constants/plugin';
export const SyntheticsCustomAssetsExtension: PackageAssetsComponent = () => {
const { http } = useKibana<ClientPluginsStart>().services;
const views: CustomAssetsAccordionProps['views'] = [
{
name: i18n.translate('xpack.uptime.fleetIntegration.assets.name', {
defaultMessage: 'Monitors',
}),
url: http?.basePath.prepend(`/app/${PLUGIN.ID}`) ?? '',
description: i18n.translate('xpack.uptime.fleetIntegration.assets.description', {
defaultMessage: 'View monitors in Uptime',
}),
},
];
return <CustomAssetsAccordion views={views} initialIsOpen />;
};