mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[Guided onboarding] State management improvements (#141278)
This commit is contained in:
parent
8ad95df6fa
commit
059fecd311
27 changed files with 1225 additions and 383 deletions
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue