[APM][ECO] Show tour for the ui service experience (#188918)

## Summary

closes https://github.com/elastic/kibana/issues/188902 



https://github.com/user-attachments/assets/d96d3d47-467a-42d2-afd2-deac402f9935
This commit is contained in:
Katerina 2024-07-26 11:32:57 +03:00 committed by GitHub
parent 6982321dbb
commit 59f7b4b3a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 258 additions and 38 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 707 KiB

After

Width:  |  Height:  |  Size: 1 MiB

Before After
Before After

View file

@ -27,6 +27,8 @@ import { ServiceListItem } from '../../../../../common/service_inventory';
import { usePreferredDataSourceAndBucketSize } from '../../../../hooks/use_preferred_data_source_and_bucket_size';
import { useProgressiveFetcher } from '../../../../hooks/use_progressive_fetcher';
import { NoEntitiesEmptyState } from './table/no_entities_empty_state';
import { Welcome } from '../../../shared/entity_enablement/welcome_modal';
import { useServiceEcoTour } from '../../../../hooks/use_eco_tour';
import { useKibana } from '../../../../context/kibana_context/use_kibana';
import { ApmPluginStartDeps, ApmServices } from '../../../../plugin';
@ -147,6 +149,7 @@ export function MultiSignalInventory() {
const [searchQuery, setSearchQuery] = React.useState('');
const { services } = useKibana<ApmPluginStartDeps & ApmServices>();
const { mainStatisticsData, mainStatisticsStatus } = useServicesEntitiesMainStatisticsFetcher();
const { tourState, hideModal } = useServiceEcoTour();
const mainStatisticsFetch = useServicesEntitiesMainStatisticsFetcher();
const initialSortField = ServiceInventoryFieldName.Throughput;
@ -172,45 +175,53 @@ export function MultiSignalInventory() {
}
}, [services.telemetry, data?.hasData]);
if (!data?.hasData && status === FETCH_STATUS.SUCCESS) {
return <NoEntitiesEmptyState />;
}
return (
<>
<EuiFlexGroup gutterSize="m">
<EuiFlexItem grow>
<TableSearchBar
placeholder={i18n.translate('xpack.apm.servicesTable.filterServicesPlaceholder', {
defaultMessage: 'Search services by name',
})}
searchQuery={searchQuery}
onChangeSearchQuery={setSearchQuery}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<SearchBar showQueryInput={false} />
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem>
<MultiSignalServicesTable
status={mainStatisticsStatus}
data={filteredData}
initialSortField={initialSortField}
initialPageSize={INITIAL_PAGE_SIZE}
initialSortDirection={INITIAL_SORT_DIRECTION}
timeseriesData={timeseriesDataFetch?.data}
timeseriesDataLoading={timeseriesDataFetch.status === FETCH_STATUS.LOADING}
noItemsMessage={
<EmptyMessage
heading={i18n.translate('xpack.apm.servicesTable.notFoundLabel', {
defaultMessage: 'No services found',
{!data?.hasData && status === FETCH_STATUS.SUCCESS ? (
<NoEntitiesEmptyState />
) : (
<>
<EuiFlexGroup gutterSize="m">
<EuiFlexItem grow>
<TableSearchBar
placeholder={i18n.translate('xpack.apm.servicesTable.filterServicesPlaceholder', {
defaultMessage: 'Search services by name',
})}
searchQuery={searchQuery}
onChangeSearchQuery={setSearchQuery}
/>
}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<SearchBar showQueryInput={false} />
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem>
<MultiSignalServicesTable
status={mainStatisticsStatus}
data={filteredData}
initialSortField={initialSortField}
initialPageSize={INITIAL_PAGE_SIZE}
initialSortDirection={INITIAL_SORT_DIRECTION}
timeseriesData={timeseriesDataFetch?.data}
timeseriesDataLoading={timeseriesDataFetch.status === FETCH_STATUS.LOADING}
noItemsMessage={
<EmptyMessage
heading={i18n.translate('xpack.apm.servicesTable.notFoundLabel', {
defaultMessage: 'No services found',
})}
/>
}
/>
</EuiFlexItem>
</EuiFlexGroup>
</>
)}
<Welcome
isModalVisible={tourState.isModalVisible ?? false}
onClose={() => hideModal()}
onConfirm={() => hideModal()}
/>
</>
);
}

View file

@ -14,6 +14,7 @@ import {
import { i18n } from '@kbn/i18n';
import React, { useState } from 'react';
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
import { useServiceEcoTour } from '../../../../hooks/use_eco_tour';
import { useKibana } from '../../../../context/kibana_context/use_kibana';
import { ApmPluginStartDeps, ApmServices } from '../../../../plugin';
import { EntityInventoryAddDataParams } from '../../../../services/telemetry';
@ -22,6 +23,7 @@ import {
collectServiceLogs,
addApmAgent,
} from '../../../shared/add_data_buttons/buttons';
import { ServiceEcoTour } from '../../../shared/entity_enablement/service_eco_tour';
const addData = i18n.translate('xpack.apm.addDataContextMenu.link', {
defaultMessage: 'Add data',
@ -29,6 +31,7 @@ const addData = i18n.translate('xpack.apm.addDataContextMenu.link', {
export function AddDataContextMenu() {
const [popoverOpen, setPopoverOpen] = useState(false);
const { tourState, hideTour } = useServiceEcoTour();
const { services } = useKibana<ApmPluginStartDeps & ApmServices>();
const {
core: {
@ -91,17 +94,23 @@ export function AddDataContextMenu() {
},
];
const handleTourClose = () => {
hideTour();
setPopoverOpen(false);
};
return (
<>
<EuiPopover
id="integrations-menu"
button={button}
isOpen={popoverOpen}
isOpen={popoverOpen || tourState.isTourActive}
closePopover={() => setPopoverOpen(false)}
panelPaddingSize="none"
anchorPosition="downRight"
>
<EuiContextMenu initialPanelId={0} panels={panels} />
<ServiceEcoTour onFinish={handleTourClose}>
<EuiContextMenu initialPanelId={0} panels={panels} />
</ServiceEcoTour>
</EuiPopover>
</>
);

View file

@ -93,7 +93,7 @@ export function CustomNoDataTemplate({
<p>
<EntityEnablement
label={i18n.translate('xpack.apm.customEmtpyState.card.link', {
defaultMessage: 'Try creating services from logs',
defaultMessage: 'Try collecting services from logs',
})}
/>
</p>

View file

@ -28,10 +28,12 @@ import { useEntityManagerEnablementContext } from '../../../context/entity_manag
import { FeedbackModal } from './feedback_modal';
import { ServiceInventoryView } from '../../../context/entity_manager_context/entity_manager_context';
import { Unauthorized } from './unauthorized_modal';
import { useServiceEcoTour } from '../../../hooks/use_eco_tour';
export function EntityEnablement({ label, tooltip }: { label: string; tooltip?: string }) {
const [isFeedbackModalVisible, setsIsFeedbackModalVisible] = useState(false);
const [isUnauthorizedModalVisible, setsIsUnauthorizedModalVisible] = useState(false);
const { tourState, showModal } = useServiceEcoTour();
const {
services: { entityManager },
@ -57,6 +59,9 @@ export function EntityEnablement({ label, tooltip }: { label: string; tooltip?:
const handleEnablement = async () => {
if (isEntityManagerEnabled) {
setServiceInventoryViewLocalStorageSetting(ServiceInventoryView.entity);
if (tourState.isModalVisible === undefined) {
showModal();
}
return;
}
@ -66,6 +71,10 @@ export function EntityEnablement({ label, tooltip }: { label: string; tooltip?:
if (response.success) {
setIsLoading(false);
setServiceInventoryViewLocalStorageSetting(ServiceInventoryView.entity);
if (tourState.isModalVisible === undefined) {
showModal();
}
refetch();
} else {
if (response.reason === ERROR_USER_NOT_AUTHORIZED) {

View file

@ -0,0 +1,49 @@
/*
* 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 { EuiText, EuiTourStep } from '@elastic/eui';
import { useServiceEcoTour } from '../../../hooks/use_eco_tour';
export function ServiceEcoTour({
children,
onFinish,
}: {
children: React.ReactElement;
onFinish: () => void;
}) {
const { tourState } = useServiceEcoTour();
return (
<EuiTourStep
content={
<EuiText>
<p>
{i18n.translate('xpack.apm.serviceEcoTour.content', {
defaultMessage: 'You can now add services from logs to the service inventory',
})}
</p>
</EuiText>
}
isStepOpen={tourState.isTourActive}
minWidth={200}
onFinish={onFinish}
step={1}
stepsTotal={1}
title={i18n.translate('xpack.apm.serviceEcoTour.title', {
defaultMessage: 'Add services from logs',
})}
subtitle={i18n.translate('xpack.apm.serviceEcoTour.subtitle', {
defaultMessage: 'New Services Inventory',
})}
anchorPosition="rightUp"
>
{children}
</EuiTourStep>
);
}

View file

@ -0,0 +1,116 @@
/*
* 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.
*/
/*
/*
* 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 {
EuiButton,
EuiConfirmModal,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiImage,
EuiLink,
EuiPanel,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { useKibanaUrl } from '../../../hooks/use_kibana_url';
export function Welcome({
isModalVisible = false,
onClose,
onConfirm,
}: {
isModalVisible: boolean;
onClose: () => void;
onConfirm: () => void;
}) {
const servicesInventory = useKibanaUrl('/plugins/apm/assets/services_inventory.png');
if (!isModalVisible) {
return null;
}
return (
<EuiConfirmModal
style={{
width: '630px',
}}
onCancel={onClose}
onConfirm={onConfirm}
confirmButtonText={
<EuiButton data-test-subj="xpack.apm.welcome.button.open" fill size="s">
{i18n.translate('xpack.apm.welcome.button.openSurvey', {
defaultMessage: 'OK',
})}
</EuiButton>
}
cancelButtonText={
<EuiLink
target="_blank"
data-test-subj="apmWelcomeLinkExternal"
href="https://ela.st/new-experience-services"
external
>
{i18n.translate('xpack.apm.welcome.linkLabel', {
defaultMessage: 'Learn more',
})}
</EuiLink>
}
defaultFocusedButton="confirm"
>
<EuiPanel hasShadow={false}>
<EuiFlexGroup direction="column" justifyContent="center" alignItems="center" gutterSize="m">
<EuiFlexItem>
<EuiIcon type="logoElastic" size="l" />
</EuiFlexItem>
<EuiFlexItem>
<EuiTitle>
<h2>
{i18n.translate('xpack.apm.welcome.title', {
defaultMessage: 'Welcome to our new experience!',
})}
</h2>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
<EuiPanel hasShadow={false} paddingSize="s">
<EuiText grow={false} textAlign="center">
<p>
{i18n.translate('xpack.apm.welcome.body', {
defaultMessage:
'You can now see services detected from logs alongside services instrumented with APM our new service inventory so you can view all of your services in a single place.',
})}
</p>
</EuiText>
</EuiPanel>
<EuiSpacer size="m" />
<EuiPanel hasBorder paddingSize="none">
<EuiImage
size="xl"
src={servicesInventory}
alt={i18n.translate('xpack.apm.welcome.image.alt', {
defaultMessage:
'Image of the new experience of the service inventory, showing services detected from logs and APM-instrumented services',
})}
/>
</EuiPanel>
</EuiConfirmModal>
);
}

View file

@ -0,0 +1,26 @@
/*
* 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 { useLocalStorage } from './use_local_storage';
type TourState = 'isModalVisible' | 'isTourActive';
const INITIAL_STATE: Record<TourState, boolean | undefined> = {
isModalVisible: undefined,
isTourActive: true,
};
export function useServiceEcoTour() {
const [tourState, setTourState] = useLocalStorage('apm.serviceEcoTour', INITIAL_STATE);
return {
tourState,
hideModal: () => setTourState({ ...tourState, isModalVisible: false }),
showModal: () => setTourState({ ...tourState, isModalVisible: true }),
hideTour: () => setTourState({ ...tourState, isTourActive: false }),
};
}