[Guided onboarding] State management improvements (#141278)

This commit is contained in:
Alison Goryachev 2022-10-03 15:46:07 -04:00 committed by GitHub
parent 8ad95df6fa
commit 059fecd311
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 1225 additions and 383 deletions

View file

@ -23,6 +23,7 @@ import { CoreStart, ScopedHistory } from '@kbn/core/public';
import { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/public/types';
import { StepTwo } from './step_two';
import { StepOne } from './step_one';
import { StepThree } from './step_three';
import { Main } from './main';
interface GuidedOnboardingExampleAppDeps {
@ -60,6 +61,9 @@ export const GuidedOnboardingExampleApp = (props: GuidedOnboardingExampleAppDeps
<Route exact path="/stepTwo">
<StepTwo guidedOnboarding={guidedOnboarding} />
</Route>
<Route exact path="/stepThree">
<StepThree guidedOnboarding={guidedOnboarding} />
</Route>
</Switch>
</Router>
</EuiPageContent>

View file

@ -25,45 +25,50 @@ import {
EuiText,
EuiTitle,
} from '@elastic/eui';
import {
import type {
GuidedOnboardingPluginStart,
GuidedOnboardingState,
UseCase,
GuideState,
GuideStepIds,
GuideId,
GuideStep,
} from '@kbn/guided-onboarding-plugin/public';
import { guidesConfig } from '@kbn/guided-onboarding-plugin/public';
interface MainProps {
guidedOnboarding: GuidedOnboardingPluginStart;
notifications: CoreStart['notifications'];
}
export const Main = (props: MainProps) => {
const {
guidedOnboarding: { guidedOnboardingApi },
notifications,
} = props;
const history = useHistory();
const [guideState, setGuideState] = useState<GuidedOnboardingState | undefined>(undefined);
const [guidesState, setGuidesState] = useState<GuideState[] | undefined>(undefined);
const [activeGuide, setActiveGuide] = useState<GuideState | undefined>(undefined);
const [selectedGuide, setSelectedGuide] = useState<
GuidedOnboardingState['activeGuide'] | undefined
>(undefined);
const [selectedStep, setSelectedStep] = useState<GuidedOnboardingState['activeStep'] | undefined>(
undefined
);
const [selectedGuide, setSelectedGuide] = useState<GuideId | undefined>(undefined);
const [selectedStep, setSelectedStep] = useState<GuideStepIds | undefined>(undefined);
useEffect(() => {
const subscription = guidedOnboardingApi
?.fetchGuideState$()
.subscribe((newState: GuidedOnboardingState) => {
setGuideState(newState);
});
return () => subscription?.unsubscribe();
const fetchGuidesState = async () => {
const newGuidesState = await guidedOnboardingApi?.fetchAllGuidesState();
setGuidesState(newGuidesState ? newGuidesState.state : []);
};
fetchGuidesState();
}, [guidedOnboardingApi]);
const startGuide = async (guide: UseCase) => {
const response = await guidedOnboardingApi?.updateGuideState({
activeGuide: guide,
activeStep: 'add_data',
});
useEffect(() => {
const newActiveGuide = guidesState?.find((guide) => guide.isActive === true);
if (newActiveGuide) {
setActiveGuide(newActiveGuide);
}
}, [guidesState, setActiveGuide]);
const activateGuide = async (guideId: GuideId, guideState?: GuideState) => {
const response = await guidedOnboardingApi?.activateGuide(guideId, guideState);
if (response) {
notifications.toasts.addSuccess(
@ -75,11 +80,45 @@ export const Main = (props: MainProps) => {
};
const updateGuideState = async () => {
const response = await guidedOnboardingApi?.updateGuideState({
activeGuide: selectedGuide!,
activeStep: selectedStep!,
const selectedGuideConfig = guidesConfig[selectedGuide!];
const selectedStepIndex = selectedGuideConfig.steps.findIndex(
(step) => step.id === selectedStep!
);
// Noop if the selected step is invalid
if (selectedStepIndex === -1) {
return;
}
const updatedSteps: GuideStep[] = selectedGuideConfig.steps.map((step, stepIndex) => {
if (selectedStepIndex > stepIndex) {
return {
id: step.id,
status: 'complete',
};
}
if (selectedStepIndex < stepIndex) {
return {
id: step.id,
status: 'inactive',
};
}
return {
id: step.id,
status: 'active',
};
});
const updatedGuideState: GuideState = {
isActive: true,
status: 'in_progress',
steps: updatedSteps,
guideId: selectedGuide!,
};
const response = await guidedOnboardingApi?.updateGuideState(updatedGuideState, true);
if (response) {
notifications.toasts.addSuccess(
i18n.translate('guidedOnboardingExample.updateGuideState.toastLabel', {
@ -116,7 +155,7 @@ export const Main = (props: MainProps) => {
so there is no need to 'load' the state from the server."
/>
</p>
{guideState ? (
{activeGuide ? (
<dl>
<dt>
<FormattedMessage
@ -124,53 +163,86 @@ export const Main = (props: MainProps) => {
defaultMessage="Active guide"
/>
</dt>
<dd>{guideState.activeGuide ?? 'undefined'}</dd>
<dd>{activeGuide.guideId}</dd>
<dt>
<FormattedMessage
id="guidedOnboardingExample.guidesSelection.state.activeStepLabel"
defaultMessage="Active step"
defaultMessage="Steps status"
/>
</dt>
<dd>{guideState.activeStep ?? 'undefined'}</dd>
<dd>
{activeGuide.steps.map((step) => {
return (
<>
{`Step "${step.id}": ${step.status}`} <br />
</>
);
})}
</dd>
</dl>
) : undefined}
) : (
<p>
<FormattedMessage
id="guidedOnboardingExample.guidesSelection.state.noActiveGuidesMessage"
defaultMessage="There are currently no active guides."
/>
</p>
)}
</EuiText>
<EuiHorizontalRule />
<EuiText>
<h3>
<FormattedMessage
id="guidedOnboardingExample.main.startGuide.title"
defaultMessage="(Re-)Start a guide"
defaultMessage="Guides"
/>
</h3>
</EuiText>
<EuiSpacer />
<EuiFlexGroup>
<EuiFlexItem>
<EuiButton onClick={() => startGuide('search')} fill>
<FormattedMessage
id="guidedOnboardingExample.guidesSelection.search.buttonLabel"
defaultMessage="(Re-)Start search guide"
/>
</EuiButton>
</EuiFlexItem>
<EuiFlexItem>
<EuiButton onClick={() => startGuide('observability')} fill>
<FormattedMessage
id="guidedOnboardingExample.guidesSelection.observability.buttonLabel"
defaultMessage="(Re-)Start observability guide"
/>
</EuiButton>
</EuiFlexItem>
<EuiFlexItem>
<EuiButton onClick={() => startGuide('security')} fill>
<FormattedMessage
id="guidedOnboardingExample.guidesSelection.security.label"
defaultMessage="(Re-)Start security guide"
/>
</EuiButton>
</EuiFlexItem>
{(Object.keys(guidesConfig) as GuideId[]).map((guideId) => {
const guideState = guidesState?.find((guide) => guide.guideId === guideId);
return (
<EuiFlexItem>
<EuiButton
onClick={() => activateGuide(guideId, guideState)}
fill
disabled={guideState?.status === 'complete'}
>
{guideState === undefined && (
<FormattedMessage
id="guidedOnboardingExample.guidesSelection.startButtonLabel"
defaultMessage="Start {guideId} guide"
values={{
guideId,
}}
/>
)}
{(guideState?.isActive === true ||
guideState?.status === 'in_progress' ||
guideState?.status === 'ready_to_complete') && (
<FormattedMessage
id="guidedOnboardingExample.guidesSelection.continueButtonLabel"
defaultMessage="Continue {guideId} guide"
values={{
guideId,
}}
/>
)}
{guideState?.status === 'complete' && (
<FormattedMessage
id="guidedOnboardingExample.guidesSelection.completeButtonLabel"
defaultMessage="Guide {guideId} complete"
values={{
guideId,
}}
/>
)}
</EuiButton>
</EuiFlexItem>
);
})}
</EuiFlexGroup>
<EuiSpacer />
<EuiHorizontalRule />
@ -187,16 +259,15 @@ export const Main = (props: MainProps) => {
<EuiFlexItem>
<EuiFormRow label="Guide" helpText="Select a guide">
<EuiSelect
id={'guideSelect'}
id="guideSelect"
options={[
{ value: 'observability', text: 'observability' },
{ value: 'security', text: 'security' },
{ value: 'search', text: 'search' },
{ value: '', text: 'unset' },
]}
value={selectedGuide}
onChange={(e) => {
const value = e.target.value as UseCase;
const value = e.target.value as GuideId;
const shouldResetState = value.trim().length === 0;
if (shouldResetState) {
setSelectedGuide(undefined);
@ -209,10 +280,10 @@ export const Main = (props: MainProps) => {
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow label="Step">
<EuiFormRow label="Step ID">
<EuiFieldText
value={selectedStep}
onChange={(e) => setSelectedStep(e.target.value)}
onChange={(e) => setSelectedStep(e.target.value as GuideStepIds)}
/>
</EuiFormRow>
</EuiFlexItem>

View file

@ -0,0 +1,90 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useEffect, useState } from 'react';
import { EuiButton, EuiSpacer, EuiText, EuiTitle, EuiTourStep } from '@elastic/eui';
import { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/public/types';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiPageContentHeader_Deprecated as EuiPageContentHeader,
EuiPageContentBody_Deprecated as EuiPageContentBody,
} from '@elastic/eui';
interface StepThreeProps {
guidedOnboarding: GuidedOnboardingPluginStart;
}
export const StepThree = (props: StepThreeProps) => {
const {
guidedOnboarding: { guidedOnboardingApi },
} = props;
const [isTourStepOpen, setIsTourStepOpen] = useState<boolean>(false);
useEffect(() => {
const subscription = guidedOnboardingApi
?.isGuideStepActive$('search', 'search_experience')
.subscribe((isStepActive) => {
setIsTourStepOpen(isStepActive);
});
return () => subscription?.unsubscribe();
}, [guidedOnboardingApi]);
return (
<>
<EuiPageContentHeader>
<EuiTitle>
<h2>
<FormattedMessage
id="guidedOnboardingExample.stepThree.title"
defaultMessage="Example step 3"
/>
</h2>
</EuiTitle>
</EuiPageContentHeader>
<EuiPageContentBody>
<EuiText>
<p>
<FormattedMessage
id="guidedOnboardingExample.guidesSelection.stepThree.explanation"
defaultMessage="The code on this page is listening to the guided setup state using an Observable subscription. If the state is set to
Search guide, step Search experience, a EUI tour will be displayed, pointing to the button below."
/>
</p>
</EuiText>
<EuiSpacer />
<EuiTourStep
content={
<EuiText>
<p>Click this button to complete step 3.</p>
</EuiText>
}
isStepOpen={isTourStepOpen}
minWidth={300}
onFinish={() => {
setIsTourStepOpen(false);
}}
step={1}
stepsTotal={1}
title="Step Build search experience"
anchorPosition="rightUp"
>
<EuiButton
onClick={async () => {
await guidedOnboardingApi?.completeGuideStep('search', 'search_experience');
}}
>
Complete step 3
</EuiButton>
</EuiTourStep>
</EuiPageContentBody>
</>
);
};

View file

@ -55,7 +55,7 @@ export const StepTwo = (props: StepTwoProps) => {
<FormattedMessage
id="guidedOnboardingExample.guidesSelection.stepTwo.explanation"
defaultMessage="The code on this page is listening to the guided setup state using an Observable subscription. If the state is set to
Search guide, step Search experience, a EUI tour will be displayed, pointing to the button below."
Search guide, step Browse documents, a EUI tour will be displayed, pointing to the button below."
/>
</p>
</EuiText>
@ -73,7 +73,7 @@ export const StepTwo = (props: StepTwoProps) => {
}}
step={1}
stepsTotal={1}
title="Step Search experience"
title="Step Browse documents"
anchorPosition="rightUp"
>
<EuiButton