[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",
"kibanaVersion": "kibana",
"requiredPlugins": [
"features",
"apmOss",
"data",
"licensing",
"triggersActionsUi",
"embeddable",
"features",
"fleet",
"infra",
"licensing",
"observability",
"ruleRegistry"
"ruleRegistry",
"triggersActionsUi"
],
"optionalPlugins": [
"spaces",
"cloud",
"usageCollection",
"taskManager",
"actions",
"alerting",
"security",
"ml",
"cloud",
"home",
"maps",
"fleet"
"ml",
"security",
"spaces",
"taskManager",
"usageCollection"
],
"server": true,
"ui": true,
"configPath": ["xpack", "apm"],
"requiredBundles": [
"fleet",
"home",
"kibanaReact",
"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,
} from '../../../../src/plugins/data/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 {
PluginSetupContract as AlertingPluginPublicSetup,
@ -43,6 +44,10 @@ import type {
} from '../../triggers_actions_ui/public';
import { registerApmAlerts } from './components/alerting/register_apm_alerts';
import { featureCatalogueEntry } from './featureCatalogueEntry';
import {
getApmEnrollmentFlyoutData,
LazyApmCustomAssetsExtension,
} from './components/fleet_integration';
export type ApmPluginSetup = ReturnType<ApmPlugin['setup']>;
@ -69,6 +74,7 @@ export interface ApmPluginStartDeps {
ml?: MlPluginStart;
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
observability: ObservabilityPublicStart;
fleet: FleetStart;
}
export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> {
@ -303,5 +309,22 @@ export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> {
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]
);
const ExtensionView = useUIExtension(packagePolicy.package?.name ?? '', 'package-policy-create');
const extensionView = useUIExtension(packagePolicy.package?.name ?? '', 'package-policy-create');
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 */}
{!ExtensionView && (
{!extensionView && (
<StepConfigurePackagePolicy
packageInfo={packageInfo}
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) */}
{ExtensionView && packagePolicy.policy_id && packagePolicy.package?.name && (
{extensionView && packagePolicy.policy_id && packagePolicy.package?.name && (
<ExtensionWrapper>
<ExtensionView newPolicy={packagePolicy} onChange={handleExtensionViewOnChange} />
<extensionView.Component
newPolicy={packagePolicy}
onChange={handleExtensionViewOnChange}
/>
</ExtensionWrapper>
)}
</>
@ -474,7 +477,7 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => {
validationResults,
formState,
integrationInfo?.name,
ExtensionView,
extensionView,
handleExtensionViewOnChange,
]
);

View file

@ -338,7 +338,7 @@ export const EditPackagePolicyForm = memo<{
packageInfo,
};
const ExtensionView = useUIExtension(packagePolicy.package?.name ?? '', 'package-policy-edit');
const extensionView = useUIExtension(packagePolicy.package?.name ?? '', 'package-policy-edit');
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 */}
{!ExtensionView && (
{!extensionView && (
<StepConfigurePackagePolicy
packageInfo={packageInfo}
packagePolicy={packagePolicy}
@ -364,12 +364,12 @@ export const EditPackagePolicyForm = memo<{
/>
)}
{ExtensionView &&
{extensionView &&
packagePolicy.policy_id &&
packagePolicy.package?.name &&
originalPackagePolicy && (
<ExtensionWrapper>
<ExtensionView
<extensionView.Component
policy={originalPackagePolicy}
newPolicy={packagePolicy}
onChange={handleExtensionViewOnChange}
@ -386,7 +386,7 @@ export const EditPackagePolicyForm = memo<{
validationResults,
formState,
originalPackagePolicy,
ExtensionView,
extensionView,
handleExtensionViewOnChange,
]
);

View file

@ -19,7 +19,7 @@ export const DisplayedAssets: ServiceNameToAssetTypes = {
elasticsearch: Object.values(ElasticsearchAssetType),
};
export type DisplayedAssetType = ElasticsearchAssetType | KibanaAssetType;
export type DisplayedAssetType = ElasticsearchAssetType | KibanaAssetType | 'view';
export const AssetTitleMap: Record<DisplayedAssetType, string> = {
dashboard: 'Dashboard',
@ -36,6 +36,7 @@ export const AssetTitleMap: Record<DisplayedAssetType, string> = {
lens: 'Lens',
security_rule: 'Security Rule',
ml_module: 'ML Module',
view: 'Views',
};
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 { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiSpacer } from '@elastic/eui';
import { Loading, Error } from '../../../../../components';
import { Loading, Error, ExtensionWrapper } from '../../../../../components';
import type { PackageInfo } 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 { allowedAssetTypes } from './constants';
@ -27,9 +32,12 @@ interface AssetsPanelProps {
export const AssetsPage = ({ packageInfo }: AssetsPanelProps) => {
const { name, version } = packageInfo;
const pkgkey = `${name}-${version}`;
const {
savedObjects: { client: savedObjectsClient },
} = useStartServices();
const customAssetsExtension = useUIExtension(packageInfo.name, 'package-detail-assets');
const { getPath } = useLink();
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
// this happens if they arrive with a direct url or they uninstall while on this tab
if (packageInstallStatus.status !== InstallStatus.installed) {
return (
<Redirect
to={getPath('integration_details_overview', {
pkgkey: `${name}-${version}`,
})}
/>
);
return <Redirect to={getPath('integration_details_overview', { pkgkey })} />;
}
let content: JSX.Element | Array<JSX.Element | null>;
@ -102,6 +104,15 @@ export const AssetsPage = ({ packageInfo }: AssetsPanelProps) => {
/>
);
} 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 = (
<EuiTitle>
<h2>
@ -112,8 +123,10 @@ export const AssetsPage = ({ packageInfo }: AssetsPanelProps) => {
</h2>
</EuiTitle>
);
}
} else {
content = allowedAssetTypes.map((assetType) => {
content = [
...allowedAssetTypes.map((assetType) => {
const sectionAssetSavedObjects = assetSavedObjects.filter((so) => so.type === assetType);
if (!sectionAssetSavedObjects.length) {
@ -126,7 +139,14 @@ export const AssetsPage = ({ packageInfo }: AssetsPanelProps) => {
<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 (

View file

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

View file

@ -17,4 +17,4 @@ export type AllowedAssetTypes = [
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 }) => {
const CustomView = useUIExtension(packageInfo.name, 'package-detail-custom');
const customViewExtension = useUIExtension(packageInfo.name, 'package-detail-custom');
const { getPath } = useLink();
const pkgkey = useMemo(() => pkgKeyFromPackageInfo(packageInfo), [packageInfo]);
return CustomView ? (
return customViewExtension ? (
<EuiFlexGroup alignItems="flexStart">
<EuiFlexItem grow={1} />
<EuiFlexItem grow={6}>
<ExtensionWrapper>
<CustomView pkgkey={pkgkey} packageInfo={packageInfo} />
<customViewExtension.Component pkgkey={pkgkey} packageInfo={packageInfo} />
</ExtensionWrapper>
</EuiFlexItem>
</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
// that is `resolved` once the lazy components actually renders.
let lazyComponentWasRendered: Promise<void>;
@ -136,7 +136,7 @@ describe('when on integration detail', () => {
testRenderer.startInterface.registerExtension({
package: 'nginx',
view: 'package-detail-custom',
component: CustomComponent,
Component: CustomComponent,
});
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', () => {
beforeEach(() => render());

View file

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

View file

@ -7,7 +7,11 @@
import { stringify, parse } from 'query-string';
import React, { memo, useCallback, useMemo, useState } from 'react';
import { Redirect, useLocation, useHistory } from 'react-router-dom';
import type { CriteriaWithPagination, EuiTableFieldDataColumnType } from '@elastic/eui';
import type {
CriteriaWithPagination,
EuiStepProps,
EuiTableFieldDataColumnType,
} from '@elastic/eui';
import {
EuiButtonIcon,
EuiBasicTable,
@ -29,6 +33,7 @@ import {
useUrlPagination,
useGetPackageInstallStatus,
AgentPolicyRefreshContext,
useUIExtension,
} from '../../../../../hooks';
import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants';
import {
@ -88,6 +93,8 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps
kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: ${name}`,
});
const agentEnrollmentFlyoutExtension = useUIExtension(name, 'agent-enrollment-flyout');
const handleTableOnChange = useCallback(
({ page }: CriteriaWithPagination<PackagePolicyAndAgentPolicy>) => {
setPagination({
@ -98,8 +105,19 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps
[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>
<FormattedMessage
@ -125,8 +143,8 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps
</EuiButton>
</>
),
[name, version, getHref]
);
};
}, [name, version, getHref, agentEnrollmentFlyoutExtension]);
const columns: Array<EuiTableFieldDataColumnType<PackagePolicyAndAgentPolicy>> = useMemo(
() => [
@ -230,13 +248,13 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps
<PackagePolicyActionsMenu
agentPolicy={agentPolicy}
packagePolicy={packagePolicy}
viewDataStepContent={renderViewDataStepContent()}
viewDataStep={viewDataStep}
/>
);
},
},
],
[renderViewDataStepContent]
[viewDataStep]
);
const noItemsMessage = useMemo(() => {
@ -292,7 +310,7 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps
data?.items.find(({ agentPolicy }) => agentPolicy.id === flyoutOpenForPolicyId)
?.agentPolicy
}
viewDataStepContent={renderViewDataStepContent()}
viewDataStep={viewDataStep}
/>
)}
</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 { AgentEnrollmentKeySelectionStep, AgentPolicySelectionStep, ViewDataStep } from './steps';
import { AgentEnrollmentKeySelectionStep, AgentPolicySelectionStep } from './steps';
import type { Props } from '.';
import { AgentEnrollmentFlyout } from '.';
@ -129,24 +129,26 @@ describe('<AgentEnrollmentFlyout />', () => {
});
});
describe('"View data" extension point', () => {
it('calls the "View data" step when UI extension is provided', async () => {
// Skipped due to implementation details in the step components. See https://github.com/elastic/kibana/issues/103894
describe.skip('"View data" extension point', () => {
it('shows the "View data" step when UI extension is provided', async () => {
jest.clearAllMocks();
await act(async () => {
testBed = await setup({
agentPolicies: [],
onClose: jest.fn(),
viewDataStepContent: <div />,
viewDataStep: { title: 'View Data', children: <div /> },
});
testBed.component.update();
});
const { exists, actions } = testBed;
expect(exists('agentEnrollmentFlyout')).toBe(true);
expect(ViewDataStep).toHaveBeenCalled();
expect(exists('view-data-step')).toBe(true);
jest.clearAllMocks();
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 () => {
@ -155,17 +157,17 @@ describe('<AgentEnrollmentFlyout />', () => {
testBed = await setup({
agentPolicies: [],
onClose: jest.fn(),
viewDataStepContent: undefined,
viewDataStep: undefined,
});
testBed.component.update();
});
const { exists, actions } = testBed;
expect(exists('agentEnrollmentFlyout')).toBe(true);
expect(ViewDataStep).not.toHaveBeenCalled();
expect(exists('view-data-step')).toBe(false);
jest.clearAllMocks();
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,
agentPolicy,
agentPolicies,
viewDataStepContent,
viewDataStep,
defaultMode = 'managed',
}) => {
const [mode, setMode] = useState<FlyoutMode>(defaultMode);
@ -119,14 +119,10 @@ export const AgentEnrollmentFlyout: React.FunctionComponent<Props> = ({
<ManagedInstructions
agentPolicy={agentPolicy}
agentPolicies={agentPolicies}
viewDataStepContent={viewDataStepContent}
viewDataStep={viewDataStep}
/>
) : (
<StandaloneInstructions
agentPolicy={agentPolicy}
agentPolicies={agentPolicies}
viewDataStepContent={viewDataStepContent}
/>
<StandaloneInstructions agentPolicy={agentPolicy} agentPolicies={agentPolicies} />
)}
</EuiFlyoutBody>
<EuiFlyoutFooter>

View file

@ -23,12 +23,7 @@ import {
} from '../../applications/fleet/sections/agents/agent_requirements_page/components';
import { FleetServerRequirementPage } from '../../applications/fleet/sections/agents/agent_requirements_page';
import {
DownloadStep,
AgentPolicySelectionStep,
AgentEnrollmentKeySelectionStep,
ViewDataStep,
} from './steps';
import { DownloadStep, AgentPolicySelectionStep, AgentEnrollmentKeySelectionStep } from './steps';
import type { BaseProps } from './types';
type Props = BaseProps;
@ -61,7 +56,7 @@ const FleetServerMissingRequirements = () => {
};
export const ManagedInstructions = React.memo<Props>(
({ agentPolicy, agentPolicies, viewDataStepContent }) => {
({ agentPolicy, agentPolicies, viewDataStep }) => {
const fleetStatus = useFleetStatus();
const [selectedApiKeyId, setSelectedAPIKeyId] = useState<string | undefined>();
@ -118,8 +113,8 @@ export const ManagedInstructions = React.memo<Props>(
});
}
if (viewDataStepContent) {
baseSteps.push(ViewDataStep(viewDataStepContent));
if (viewDataStep) {
baseSteps.push({ 'data-test-subj': 'view-data-step', ...viewDataStep });
}
return baseSteps;
@ -127,12 +122,12 @@ export const ManagedInstructions = React.memo<Props>(
agentPolicy,
selectedApiKeyId,
setSelectedAPIKeyId,
viewDataStepContent,
agentPolicies,
apiKey.data,
fleetServerSteps,
isFleetServerPolicySelected,
settings.data?.item?.fleet_server_hosts,
viewDataStep,
]);
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.
*/
import type { EuiStepProps } from '@elastic/eui';
import type { AgentPolicy } from '../../types';
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
* 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 { EuiContextMenuItem, EuiPortal } from '@elastic/eui';
import type { EuiStepProps } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import type { AgentPolicy, PackagePolicy } from '../types';
@ -21,8 +22,8 @@ import { PackagePolicyDeleteProvider } from './package_policy_delete_provider';
export const PackagePolicyActionsMenu: React.FunctionComponent<{
agentPolicy: AgentPolicy;
packagePolicy: PackagePolicy;
viewDataStepContent?: JSX.Element;
}> = ({ agentPolicy, packagePolicy, viewDataStepContent }) => {
viewDataStep?: EuiStepProps;
}> = ({ agentPolicy, packagePolicy, viewDataStep }) => {
const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState(false);
const { getHref } = useLink();
const hasWriteCapabilities = useCapabilities().write;
@ -106,7 +107,7 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{
<EuiPortal>
<AgentEnrollmentFlyout
agentPolicy={agentPolicy}
viewDataStepContent={viewDataStepContent}
viewDataStep={viewDataStep}
onClose={onEnrollmentFlyoutClose}
/>
</EuiPortal>

View file

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

View file

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

View file

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

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import type { EuiStepProps } from '@elastic/eui';
import type { ComponentType, LazyExoticComponent } from 'react';
import type { NewPackagePolicy, PackageInfo, PackagePolicy } from './index';
@ -48,7 +49,7 @@ export interface PackagePolicyEditExtensionComponentProps {
export interface PackagePolicyEditExtension {
package: string;
view: 'package-policy-edit';
component: LazyExoticComponent<PackagePolicyEditExtensionComponent>;
Component: LazyExoticComponent<PackagePolicyEditExtensionComponent>;
}
/**
@ -76,7 +77,7 @@ export interface PackagePolicyCreateExtensionComponentProps {
export interface PackagePolicyCreateExtension {
package: string;
view: 'package-policy-create';
component: LazyExoticComponent<PackagePolicyCreateExtensionComponent>;
Component: LazyExoticComponent<PackagePolicyCreateExtensionComponent>;
}
/**
@ -94,11 +95,32 @@ export interface PackageCustomExtensionComponentProps {
export interface PackageCustomExtension {
package: string;
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 */
export type UIExtensionPoint =
| PackagePolicyEditExtension
| PackageCustomExtension
| PackagePolicyCreateExtension;
| PackagePolicyCreateExtension
| PackageAssetsExtension
| AgentEnrollmentFlyoutFinalStepExtension;

View file

@ -58,7 +58,7 @@ export function toggleOsqueryPlugin(
registerExtension({
package: OSQUERY_INTEGRATION_NAME,
view: 'package-detail-custom',
component: LazyOsqueryManagedCustomButtonExtension,
Component: LazyOsqueryManagedCustomButtonExtension,
});
}
@ -146,13 +146,13 @@ export class OsqueryPlugin implements Plugin<OsqueryPluginSetup, OsqueryPluginSt
registerExtension({
package: OSQUERY_INTEGRATION_NAME,
view: 'package-policy-create',
component: LazyOsqueryManagedPolicyCreateImportExtension,
Component: LazyOsqueryManagedPolicyCreateImportExtension,
});
registerExtension({
package: OSQUERY_INTEGRATION_NAME,
view: 'package-policy-edit',
component: LazyOsqueryManagedPolicyEditExtension,
Component: LazyOsqueryManagedPolicyEditExtension,
});
}
} 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 { parseExperimentalConfigValue } from '../common/experimental_features';
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> {
private kibanaVersion: string;
@ -199,19 +200,25 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
registerExtension({
package: 'endpoint',
view: 'package-policy-edit',
component: getLazyEndpointPolicyEditExtension(core, plugins),
Component: getLazyEndpointPolicyEditExtension(core, plugins),
});
registerExtension({
package: 'endpoint',
view: 'package-policy-create',
component: LazyEndpointPolicyCreateExtension,
Component: LazyEndpointPolicyCreateExtension,
});
registerExtension({
package: 'endpoint',
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$);

View file

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

View file

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