mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
6982321dbb
commit
59f7b4b3a5
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 |
|
@ -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()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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 }),
|
||||
};
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue