[Guided onboarding] Update observability tour (#143006)

This commit is contained in:
Alison Goryachev 2022-10-19 08:16:49 -04:00 committed by GitHub
parent b232f35fd5
commit 048b11d274
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 84 additions and 67 deletions

View file

@ -48,7 +48,7 @@ export const Main = (props: MainProps) => {
const [guidesState, setGuidesState] = useState<GuideState[] | undefined>(undefined);
const [activeGuide, setActiveGuide] = useState<GuideState | undefined>(undefined);
const [selectedGuide, setSelectedGuide] = useState<GuideId | undefined>(undefined);
const [selectedGuide, setSelectedGuide] = useState<GuideId | undefined>('observability');
const [selectedStep, setSelectedStep] = useState<GuideStepIds | undefined>(undefined);
useEffect(() => {

View file

@ -79,9 +79,13 @@ export const observabilityConfig: GuideConfig = {
descriptionList: [
i18n.translate('guidedOnboarding.observabilityGuide.tourObservabilityStep.description', {
defaultMessage:
'Take a look at the capabilities of our Observability solution and be inspired to add more integrations.',
'Get familiar with the rest of Elastic Observability and explore even more integrations.',
}),
],
location: {
appID: 'observability',
path: '/overview',
},
},
],
};

View file

@ -160,7 +160,7 @@ export class ApiService implements GuidedOnboardingApi {
/**
* Marks a guide as inactive
* This is useful for the dropdown panel, when a user quits a guide
* @param {GuideState} guide (optional) the selected guide state, if it exists (i.e., if a user is continuing a guide)
* @param {GuideState} guide the selected guide state
* @return {Promise} a promise with the updated guide state
*/
public async deactivateGuide(guide: GuideState): Promise<{ state: GuideState } | undefined> {

View file

@ -49,4 +49,4 @@
"observability",
"esUiShared"
]
}
}

View file

@ -31,7 +31,8 @@
"triggersActionsUi",
"inspector",
"unifiedSearch",
"security"
"security",
"guidedOnboarding"
],
"ui": true,
"server": true,

View file

@ -11,6 +11,7 @@ import { shallow } from 'enzyme';
import React from 'react';
import { of } from 'rxjs';
import { getKibanaPageTemplateKibanaDependenciesMock as getPageTemplateServices } from '@kbn/shared-ux-page-kibana-template-mocks';
import { guidedOnboardingMock } from '@kbn/guided-onboarding-plugin/public/mocks';
import { createNavigationRegistry } from '../../../services/navigation_registry';
import { createLazyObservabilityPageTemplate } from './lazy_page_template';
@ -54,6 +55,7 @@ describe('Page template', () => {
navigateToApp: async () => {},
navigationSections$: navigationRegistry.sections$,
getPageTemplateServices,
guidedOnboardingApi: guidedOnboardingMock.createStart().guidedOnboardingApi,
});
const component = shallow(
@ -82,6 +84,7 @@ describe('Page template', () => {
rightSideItems: [<span>Test side item</span>],
}}
getPageTemplateServices={getPageTemplateServices}
guidedOnboardingApi={guidedOnboardingMock.createStart().guidedOnboardingApi}
>
<div>Test structure</div>
</ObservabilityPageTemplate>
@ -103,6 +106,7 @@ describe('Page template', () => {
rightSideItems: [<span>Test side item</span>],
}}
getPageTemplateServices={getPageTemplateServices}
guidedOnboardingApi={guidedOnboardingMock.createStart().guidedOnboardingApi}
>
<div>Test structure</div>
</ObservabilityPageTemplate>

View file

@ -22,6 +22,8 @@ import type {
KibanaPageTemplateProps,
KibanaPageTemplateKibanaDependencies,
} from '@kbn/shared-ux-page-kibana-template';
import { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/public';
import { ObservabilityAppServices } from '../../../application/types';
import type { NavigationSection } from '../../../services/navigation_registry';
import { ObservabilityTour } from '../tour';
import { NavNameWithBadge, hideBadge } from './nav_name_with_badge';
@ -49,6 +51,7 @@ export interface ObservabilityPageTemplateDependencies {
navigateToApp: ApplicationStart['navigateToApp'];
navigationSections$: Observable<NavigationSection[]>;
getPageTemplateServices: () => KibanaPageTemplateKibanaDependencies;
guidedOnboardingApi: GuidedOnboardingPluginStart['guidedOnboardingApi'];
}
export type ObservabilityPageTemplateProps = ObservabilityPageTemplateDependencies &
@ -66,13 +69,14 @@ export function ObservabilityPageTemplate({
bottomBar,
bottomBarProps,
pageSectionProps,
guidedOnboardingApi,
...pageTemplateProps
}: ObservabilityPageTemplateProps): React.ReactElement | null {
const sections = useObservable(navigationSections$, []);
const currentAppId = useObservable(currentAppId$, undefined);
const { pathname: currentPath } = useLocation();
const { services } = useKibana();
const { services } = useKibana<ObservabilityAppServices>();
const sideNavItems = useMemo<Array<EuiSideNavItemType<unknown>>>(
() =>
@ -141,6 +145,7 @@ export function ObservabilityPageTemplate({
<ObservabilityTour
navigateToApp={navigateToApp}
prependBasePath={services?.http?.basePath.prepend}
guidedOnboardingApi={guidedOnboardingApi}
isPageDataLoaded={isPageDataLoaded}
// The tour is dependent on the solution nav, and should not render if it is not visible
showTour={showSolutionNav}

View file

@ -5,9 +5,4 @@
* 2.0.
*/
export {
ObservabilityTour,
observTourActiveStorageKey,
observTourStepStorageKey,
useObservabilityTourContext,
} from './tour';
export { ObservabilityTour, observTourStepStorageKey, useObservabilityTourContext } from './tour';

View file

@ -108,11 +108,11 @@ export const tourStepsConfig: TourStep[] = [
},
{
title: i18n.translate('xpack.observability.tour.guidedSetupStep.tourTitle', {
defaultMessage: 'Now add your data!',
defaultMessage: 'Do more with Elastic Observability',
}),
content: i18n.translate('xpack.observability.tour.guidedSetupStep.tourContent', {
defaultMessage:
'The easiest way to get going with Elastic Observability is to follow the steps in the data assistant.',
'The easiest way to continue with Elastic Observability is to follow recommended next steps in the data assistant.',
}),
anchor: '#guidedSetupButton',
anchorPosition: 'rightUp',

View file

@ -29,6 +29,9 @@ import {
} from '@elastic/eui';
import { useLocation } from 'react-router-dom';
import { ApplicationStart } from '@kbn/core/public';
import useObservable from 'react-use/lib/useObservable';
import { of } from 'rxjs';
import type { GuidedOnboardingApi } from '@kbn/guided-onboarding-plugin/public/types';
import { observabilityAppId } from '../../../../common';
import { tourStepsConfig } from './steps_config';
@ -38,9 +41,8 @@ const offset: EuiTourStepProps['offset'] = 30;
const repositionOnScroll: EuiTourStepProps['repositionOnScroll'] = true;
const overviewPath = '/overview';
const guidedSetupStep = 6;
const dataAssistantStep = 6;
export const observTourActiveStorageKey = 'guidedOnboarding.observability.tourActive';
export const observTourStepStorageKey = 'guidedOnboarding.observability.tourStep';
const getSteps = ({
@ -140,35 +142,6 @@ const getSteps = ({
});
};
interface TourState {
activeStep: number;
isTourActive: boolean;
}
const getInitialTourState = ({
prevIsTourActive,
prevActiveStep,
}: {
prevIsTourActive: string | null;
prevActiveStep: string | null;
}): TourState => {
if (prevIsTourActive === null) {
return {
activeStep: 1,
// Tour is inactive by default
isTourActive: false,
};
}
const isTourActive = prevIsTourActive === 'true';
const activeStep = prevActiveStep === null ? 1 : Number(prevActiveStep);
return {
activeStep,
isTourActive,
};
};
export interface ObservabilityTourContextValue {
endTour: () => void;
isTourVisible: boolean;
@ -185,22 +158,26 @@ export function ObservabilityTour({
isPageDataLoaded,
showTour,
prependBasePath,
guidedOnboardingApi,
}: {
children: ({ isTourVisible }: { isTourVisible: boolean }) => ReactNode;
navigateToApp: ApplicationStart['navigateToApp'];
isPageDataLoaded: boolean;
showTour: boolean;
prependBasePath?: (imageName: string) => string;
guidedOnboardingApi?: GuidedOnboardingApi;
}) {
const prevIsTourActive = localStorage.getItem(observTourActiveStorageKey);
const prevActiveStep = localStorage.getItem(observTourStepStorageKey);
const initialActiveStep = prevActiveStep === null ? 1 : Number(prevActiveStep);
const { activeStep: initialActiveStep, isTourActive: initialIsTourActive } = getInitialTourState({
prevIsTourActive,
prevActiveStep,
});
const isGuidedOnboardingActive = useObservable(
// if guided onboarding is not available, return false
guidedOnboardingApi
? guidedOnboardingApi.isGuideStepActive$('observability', 'tour_observability')
: of(false)
);
const [isTourActive, setIsTourActive] = useState(initialIsTourActive);
const [isTourActive, setIsTourActive] = useState(false);
const [activeStep, setActiveStep] = useState(initialActiveStep);
const { pathname: currentPath } = useLocation();
@ -213,16 +190,19 @@ export function ObservabilityTour({
setActiveStep((prevState) => prevState + 1);
}, []);
const endTour = useCallback(() => {
// Reset tour state
setIsTourActive(false);
const endTour = useCallback(async () => {
// Mark the onboarding guide step as complete
if (guidedOnboardingApi) {
await guidedOnboardingApi.completeGuideStep('observability', 'tour_observability');
}
// Reset EuiTour step state
setActiveStep(1);
}, []);
}, [guidedOnboardingApi]);
/**
* The tour should only be visible if the following conditions are met:
* - Only pages with the side nav should show the tour (showTour === true)
* - Tour is set to active per localStorage setting (isTourActive === true)
* - Tour is set to active per the guided onboarding service (isTourActive === true)
* - Any page data must be loaded in order for the tour to render correctly
* - The tour should only render on medium-large screens
*/
@ -230,17 +210,17 @@ export function ObservabilityTour({
const context: ObservabilityTourContextValue = { endTour, isTourVisible };
useEffect(() => {
localStorage.setItem(observTourActiveStorageKey, String(isTourActive));
}, [isTourActive]);
useEffect(() => {
localStorage.setItem(observTourStepStorageKey, String(activeStep));
}, [activeStep]);
useEffect(() => {
setIsTourActive(Boolean(isGuidedOnboardingActive));
}, [isGuidedOnboardingActive]);
useEffect(() => {
// The user must be on the overview page to view the data assistant step in the tour
if (isTourActive && isOverviewPage === false && activeStep === guidedSetupStep) {
if (isTourActive && isOverviewPage === false && activeStep === dataAssistantStep) {
navigateToApp(observabilityAppId, {
path: overviewPath,
});

View file

@ -37,6 +37,7 @@ import {
RuleTypeRegistryContract,
} from '@kbn/triggers-actions-ui-plugin/public';
import { SecurityPluginStart } from '@kbn/security-plugin/public';
import { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/public';
import { observabilityAppId, observabilityFeatureId, casesPath } from '../common';
import { createLazyObservabilityPageTemplate } from './components/shared';
import { registerDataHandler } from './data_handler';
@ -92,6 +93,7 @@ export interface ObservabilityPublicPluginsStart {
ruleTypeRegistry: RuleTypeRegistryContract;
actionTypeRegistry: ActionTypeRegistryContract;
security: SecurityPluginStart;
guidedOnboarding: GuidedOnboardingPluginStart;
}
export type ObservabilityPublicStart = ReturnType<Plugin['start']>;
@ -300,6 +302,7 @@ export class Plugin
getUrlForApp: application.getUrlForApp,
navigateToApp: application.navigateToApp,
navigationSections$: this.navigationRegistry.sections$,
guidedOnboardingApi: pluginsStart.guidedOnboarding.guidedOnboardingApi,
getPageTemplateServices: () => ({ coreStart }),
});

View file

@ -30,5 +30,6 @@
{ "path": "../timelines/tsconfig.json"},
{ "path": "../translations/tsconfig.json" },
{ "path": "../../../src/plugins/unified_search/tsconfig.json"},
{ "path": "../../../src/plugins/guided_onboarding/tsconfig.json"},
]
}

View file

@ -6,10 +6,7 @@
*/
import expect from '@kbn/expect';
import {
observTourActiveStorageKey,
observTourStepStorageKey,
} from '@kbn/observability-plugin/public/components/shared/tour';
import { observTourStepStorageKey } from '@kbn/observability-plugin/public/components/shared/tour';
import { FtrProviderContext } from '../../ftr_provider_context';
export default ({ getPageObjects, getService }: FtrProviderContext) => {
@ -17,9 +14,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const esArchiver = getService('esArchiver');
const pageObjects = getPageObjects(['common', 'infraHome']);
const find = getService('find');
const supertest = getService('supertest');
const setInitialTourState = async (activeStep?: number) => {
await browser.setLocalStorageItem(observTourActiveStorageKey, 'true');
await browser.setLocalStorageItem(observTourStepStorageKey, String(activeStep || 1));
await browser.refresh();
};
@ -36,11 +33,37 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
after(async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs');
await browser.removeLocalStorageItem(observTourActiveStorageKey);
await browser.removeLocalStorageItem(observTourStepStorageKey);
});
describe('Tour enabled', () => {
beforeEach(async () => {
// Activate the Observability guide, step 3, in order to trigger the EuiTour
await supertest
.put(`/api/guided_onboarding/state`)
.set('kbn-xsrf', 'true')
.send({
status: 'in_progress',
guideId: 'observability',
isActive: true,
steps: [
{
id: 'add_data',
status: 'complete',
},
{
id: 'view_dashboard',
status: 'complete',
},
{
id: 'tour_observability',
status: 'in_progress',
},
],
})
.expect(200);
});
it('can complete tour', async () => {
await setInitialTourState();

View file

@ -52,6 +52,7 @@ export default async function ({ readConfigFile }) {
'--xpack.discoverEnhanced.actions.exploreDataInContextMenu.enabled=true',
'--savedObjects.maxImportPayloadBytes=10485760', // for OSS test management/_import_objects,
'--uiSettings.overrides.observability:enableNewSyntheticsView=true', // for OSS test management/_import_objects,
'--guidedOnboarding.ui=true', // Enable guided onboarding for infra/tour.ts tests
],
},
uiSettings: {