[APM][ECO] Promote new experience when no apm data found (#188867)

closes https://github.com/elastic/observability-dev/issues/3737

## Summary

- When FF is disabled it shows the existing no data page
- The no data config is very limited in the template, thus we had to go
against the guidelines and create a custom no data page for the new
experience.
- The user needs to have permissions to enable EEM, othewise same modal
appears with slightly different copy

Additionally, the PR includes
- Small refactoring in the enablement component in order to share it
- Add short link that was misseed


https://github.com/user-attachments/assets/5d3bbe83-682a-47a1-a9af-770f1ca42876

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Katerina 2024-07-23 18:01:40 +03:00 committed by GitHub
parent 2cc03329e4
commit 5d9d92b88e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 232 additions and 56 deletions

View file

@ -11,6 +11,7 @@ import { entityCentricExperience } from '@kbn/observability-plugin/common';
import { ObservabilityPageTemplateProps } from '@kbn/observability-shared-plugin/public';
import type { KibanaPageTemplateProps } from '@kbn/shared-ux-page-kibana-template';
import React, { useContext } from 'react';
import { i18n } from '@kbn/i18n';
import { useLocation } from 'react-router-dom';
import { FeatureFeedbackButton } from '@kbn/observability-shared-plugin/public';
import { useEntityManagerEnablementContext } from '../../../context/entity_manager_context/use_entity_manager_enablement_context';
@ -26,6 +27,8 @@ import { ApmEnvironmentFilter } from '../../shared/environment_filter';
import { getNoDataConfig } from './no_data_config';
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
import { EntityEnablement } from '../../shared/entity_enablement';
import { CustomNoDataTemplate } from './custom_no_data_template';
import { ServiceInventoryView } from '../../../context/entity_manager_context/entity_manager_context';
// Paths that must skip the no data screen
const bypassNoDataScreenPaths = ['/settings', '/diagnostics'];
@ -76,7 +79,8 @@ export function ApmMainTemplate({
entityCentricExperience,
false
);
const { isEntityCentricExperienceViewEnabled } = useEntityManagerEnablementContext();
const { isEntityCentricExperienceViewEnabled, serviceInventoryViewLocalStorageSetting } =
useEntityManagerEnablementContext();
const ObservabilityPageTemplate = observabilityShared.navigation.PageTemplate;
@ -114,6 +118,10 @@ export function ApmMainTemplate({
const hasApmData = !!data?.hasData;
const hasApmIntegrations = !!fleetApmPoliciesData?.hasApmPolicies;
const showCustomEmptyState =
!hasApmData &&
isEntityCentricExperienceSettingEnabled &&
serviceInventoryViewLocalStorageSetting === ServiceInventoryView.classic;
const noDataConfig = getNoDataConfig({
basePath,
@ -160,9 +168,16 @@ export function ApmMainTemplate({
</EuiFlexGroup>
);
const pageTemplate = (
const pageTemplate = showCustomEmptyState ? (
<CustomNoDataTemplate isPageDataLoaded={isLoading === false} noDataConfig={noDataConfig} />
) : (
<ObservabilityPageTemplate
noDataConfig={shouldBypassNoDataScreen ? undefined : noDataConfig}
noDataConfig={
shouldBypassNoDataScreen ||
serviceInventoryViewLocalStorageSetting === ServiceInventoryView.entity
? undefined
: noDataConfig
}
isPageDataLoaded={isLoading === false}
pageHeader={{
rightSideItems,
@ -173,7 +188,15 @@ export function ApmMainTemplate({
{isEntityCentricExperienceSettingEnabled &&
showEnablementCallout &&
selectedNavButton === 'allServices' ? (
<EntityEnablement />
<EntityEnablement
label={i18n.translate('xpack.apm.eemEnablement.tryItButton.', {
defaultMessage: 'Try our new experience!',
})}
tooltip={i18n.translate('xpack.apm.entityEnablement.content', {
defaultMessage:
'Our new experience combines both APM-instrumented services with services detected from logs in a single service inventory.',
})}
/>
) : null}
{showServiceGroupsNav && selectedNavButton && (
<ServiceGroupsButtonGroup selectedNavButton={selectedNavButton} />

View file

@ -0,0 +1,124 @@
/*
* 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 {
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiTextColor,
EuiText,
EuiButton,
EuiPageTemplate,
EuiCard,
EuiImage,
EuiScreenReaderOnly,
} from '@elastic/eui';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { KibanaSolutionAvatar } from '@kbn/shared-ux-avatar-solution';
import { NoDataConfig } from '@kbn/shared-ux-page-no-data-config-types';
import { ApmPluginStartDeps } from '../../../plugin';
import { EntityEnablement } from '../../shared/entity_enablement';
export function CustomNoDataTemplate({
isPageDataLoaded,
noDataConfig,
}: {
isPageDataLoaded: boolean;
noDataConfig?: NoDataConfig;
}) {
const { services } = useKibana<ApmPluginStartDeps>();
const { http, observabilityShared } = services;
const basePath = http?.basePath.get();
const ObservabilityPageTemplate = observabilityShared.navigation.PageTemplate;
const imageUrl = `${basePath}/plugins/kibanaReact/assets/elastic_agent_card.svg`;
return (
<ObservabilityPageTemplate isPageDataLoaded={isPageDataLoaded} paddingSize="none">
<EuiPageTemplate panelled={false} offset={0} restrictWidth="960px">
<EuiPageTemplate.Section alignment="center" component="div" grow>
<EuiText textAlign="center">
<KibanaSolutionAvatar name="observability" iconType="logoObservability" size="xxl" />
<EuiSpacer size="l" />
<h1>
{i18n.translate('xpack.apm.customEmtpyState.title', {
defaultMessage: 'Detect and resolve problems with your application',
})}
</h1>
<EuiTextColor color="subdued">
<p>
{i18n.translate('xpack.apm.customEmtpyState.description', {
defaultMessage:
'Start collecting data for your applications and services so you can detect and resolve problems faster.',
})}
</p>
</EuiTextColor>
</EuiText>
<EuiSpacer size="xxl" />
<EuiCard
css={{ maxWidth: 400, marginInline: 'auto' }}
paddingSize="l"
title={
<EuiScreenReaderOnly>
<span>
{i18n.translate('xpack.apm.customEmtpyState.title.reader', {
defaultMessage: 'Add APM data',
})}
</span>
</EuiScreenReaderOnly>
}
description={i18n.translate('xpack.apm.customEmtpyState.card.description', {
defaultMessage:
'Use APM agents to collect APM data. We make it easy with agents for many popular languages.',
})}
footer={
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="apmCustomNoDataTemplateAddApmAgentButton"
color="primary"
fill
href={noDataConfig?.action.elasticAgent.href}
>
{noDataConfig?.action.elasticAgent.title}
</EuiButton>
<EuiSpacer size="m" />
<EuiText size="s">
<p>
<EntityEnablement
label={i18n.translate('xpack.apm.customEmtpyState.card.link', {
defaultMessage: 'Try creating services from logs',
})}
/>
</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
}
image={
<EuiImage
size="fullWidth"
style={{
width: 'max(100%, 360px)',
height: 240,
objectFit: 'cover',
background: 'aliceblue',
}}
url={imageUrl}
alt={i18n.translate('xpack.apm.customEmtpyState.img.alt', {
defaultMessage: 'Image of the Elastic Agent card',
})}
/>
}
/>
</EuiPageTemplate.Section>
</EuiPageTemplate>
</ObservabilityPageTemplate>
);
}

View file

@ -28,7 +28,7 @@ import { FeedbackModal } from './feedback_modal';
import { ServiceInventoryView } from '../../../context/entity_manager_context/entity_manager_context';
import { Unauthorized } from './unauthorized_modal';
export function EntityEnablement() {
export function EntityEnablement({ label, tooltip }: { label: string; tooltip?: string }) {
const [isFeedbackModalVisible, setsIsFeedbackModalVisible] = useState(false);
const [isUnauthorizedModalVisible, setsIsUnauthorizedModalVisible] = useState(false);
@ -37,6 +37,7 @@ export function EntityEnablement() {
} = useKibana<ApmPluginStartDeps>();
const {
isEntityManagerEnabled,
isEnablementPending,
refetch,
setServiceInventoryViewLocalStorageSetting,
@ -52,6 +53,11 @@ export function EntityEnablement() {
};
const handleEnablement = async () => {
if (isEntityManagerEnabled) {
setServiceInventoryViewLocalStorageSetting(ServiceInventoryView.entity);
return;
}
setIsLoading(true);
try {
const response = await entityManager.entityClient.enableManagedEntityDiscovery();
@ -82,11 +88,15 @@ export function EntityEnablement() {
) : (
<EuiFlexGroup direction="row" alignItems="center" gutterSize="xs">
<EuiFlexItem grow={false}>
{isLoading ? <EuiLoadingSpinner size="m" /> : <TechnicalPreviewBadge icon="beaker" />}
{isLoading ? (
<EuiLoadingSpinner size="m" />
) : (
<TechnicalPreviewBadge icon="beaker" style={{ verticalAlign: 'middle' }} />
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiLink
disabled={isEntityCentricExperienceViewEnabled}
disabled={isEntityCentricExperienceViewEnabled || isLoading}
data-test-subj="tryOutEEMLink"
onClick={handleEnablement}
>
@ -94,54 +104,54 @@ export function EntityEnablement() {
? i18n.translate('xpack.apm.eemEnablement.enabled.', {
defaultMessage: 'Viewing our new experience',
})
: i18n.translate('xpack.apm.eemEnablement.tryItButton.', {
defaultMessage: 'Try our new experience!',
})}
: label}
</EuiLink>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPopover
button={
<EuiButtonIcon
onClick={togglePopover}
data-test-subj="apmEntityEnablementWithFooterButton"
iconType="iInCircle"
size="xs"
aria-label={i18n.translate('xpack.apm.entityEnablement.euiButtonIcon.arial', {
defaultMessage: 'click to find more for the new ui experience',
})}
/>
}
isOpen={isPopoverOpen}
closePopover={togglePopover}
anchorPosition="downLeft"
>
<div style={{ width: '300px' }}>
<EuiText size="s">
<p>
{i18n.translate('xpack.apm.entityEnablement.content', {
defaultMessage:
'Our new experience combines both APM-instrumented services with services detected from logs in a single service inventory.',
{tooltip && (
<EuiFlexItem grow={false}>
<EuiPopover
button={
<EuiButtonIcon
onClick={togglePopover}
data-test-subj="apmEntityEnablementWithFooterButton"
iconType="iInCircle"
size="xs"
aria-label={i18n.translate('xpack.apm.entityEnablement.euiButtonIcon.arial', {
defaultMessage: 'click to find more for the new ui experience',
})}
</p>
</EuiText>
</div>
<EuiPopoverFooter>
<EuiTextColor color="subdued">
<EuiLink
data-test-subj="apmEntityEnablementLink"
href="https://ela.st/new-experience-services"
external
target="_blank"
>
{i18n.translate('xpack.apm.entityEnablement.footer', {
defaultMessage: 'Learn more',
})}
</EuiLink>
</EuiTextColor>
</EuiPopoverFooter>
</EuiPopover>
</EuiFlexItem>
/>
}
isOpen={isPopoverOpen}
closePopover={togglePopover}
anchorPosition="downLeft"
>
<div style={{ width: '300px' }}>
<EuiText size="s">
<p>
{i18n.translate('xpack.apm.entityEnablement.content', {
defaultMessage:
'Our new experience combines both APM-instrumented services with services detected from logs in a single service inventory.',
})}
</p>
</EuiText>
</div>
<EuiPopoverFooter>
<EuiTextColor color="subdued">
<EuiLink
data-test-subj="apmEntityEnablementLink"
href="https://ela.st/new-experience-services"
external
target="_blank"
>
{i18n.translate('xpack.apm.entityEnablement.footer', {
defaultMessage: 'Learn more',
})}
</EuiLink>
</EuiTextColor>
</EuiPopoverFooter>
</EuiPopover>
</EuiFlexItem>
)}
{isEntityCentricExperienceViewEnabled && (
<EuiFlexItem grow={false}>
<EuiLink data-test-subj="restoreClassicView" onClick={handleRestoreView}>
@ -158,6 +168,7 @@ export function EntityEnablement() {
<Unauthorized
isUnauthorizedModalVisible={isUnauthorizedModalVisible}
onClose={() => setsIsUnauthorizedModalVisible(false)}
label={label}
/>
</EuiFlexGroup>
);

View file

@ -22,6 +22,7 @@ import {
EuiFlexItem,
EuiIcon,
EuiImage,
EuiLink,
EuiPanel,
EuiSpacer,
EuiText,
@ -32,9 +33,11 @@ import { useKibanaUrl } from '../../../hooks/use_kibana_url';
export function Unauthorized({
isUnauthorizedModalVisible = false,
onClose,
label,
}: {
isUnauthorizedModalVisible?: boolean;
onClose: () => void;
label: string;
}) {
const servicesInventory = useKibanaUrl('/plugins/apm/assets/services_inventory.png');
@ -54,6 +57,18 @@ export function Unauthorized({
})}
</EuiButton>
}
cancelButtonText={
<EuiLink
target="_blank"
data-test-subj="apmUnauthorizedLinkExternal"
href="https://ela.st/new-experience-services"
external
>
{i18n.translate('xpack.apm.unauthorized.linkLinkLabel', {
defaultMessage: 'See how to enable EEM',
})}
</EuiLink>
}
defaultFocusedButton="confirm"
>
<EuiPanel hasShadow={false}>
@ -83,7 +98,8 @@ export function Unauthorized({
<p>
{i18n.translate('xpack.apm.unauthorised.body', {
defaultMessage:
'To see services detected from logs and APM-instrumented services in our new service inventory, please ask an administrator to visit this page and try our new experience. ',
'To see services detected from logs and APM-instrumented services in our new service inventory, please ask an administrator to visit this page and {label}. ',
values: { label: label.toLowerCase() },
})}
</p>
</EuiText>

View file

@ -123,7 +123,9 @@
"@kbn/test-jest-helpers",
"@kbn/security-plugin-types-common",
"@kbn/entityManager-plugin",
"@kbn/react-hooks"
"@kbn/react-hooks",
"@kbn/shared-ux-avatar-solution",
"@kbn/shared-ux-page-no-data-config-types"
],
"exclude": [
"target/**/*"

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { EuiSideNavItemType, EuiPageSectionProps } from '@elastic/eui';
import { EuiSideNavItemType, EuiPageSectionProps, EuiEmptyPromptProps } from '@elastic/eui';
import { _EuiPageBottomBarProps } from '@elastic/eui/src/components/page_template/bottom_bar/page_bottom_bar';
import { i18n } from '@kbn/i18n';
import React, { useMemo } from 'react';
@ -39,7 +39,7 @@ export type WrappedPageTemplateProps = Pick<
bottomBar?: React.ReactNode;
bottomBarProps?: _EuiPageBottomBarProps;
topSearchBar?: React.ReactNode;
};
} & Pick<EuiEmptyPromptProps, 'paddingSize'>;
export interface NavigationEntry {
// the label of the menu entry, should be translated