[Onboarding] Create guided_onboarding plugin (#138611)

* [Guided onboarding] Smashed commit of all POC work for guided onboarding and guided onboarding example plugins

* [Guided onboarding] Fixed type errors

* [Guided onboarding] Removed guidedOnboardingExample limit

* [Guided onboarding] Fixed a functonal test for exposed configs

* [Guided onboarding] Fixed plugin limit

* [Guided onboarding] Added more information to the example plugin

* [Guided onboarding] Fixed no-console error

* [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix'

* [Guided onboarding] Fixed snake case errors

* move guided_onboarding out of x-pack

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Alison Goryachev <alison.goryachev@elastic.co>
This commit is contained in:
Yulia Čech 2022-09-15 11:35:35 +02:00 committed by GitHub
parent 7d6b5c6eb2
commit 95086f4365
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 1666 additions and 0 deletions

View file

@ -0,0 +1,7 @@
{
"prefix": "guidedOnboardingExample",
"paths": {
"guidedOnboardingExample": "."
},
"translations": ["translations/ja-JP.json"]
}

View file

@ -0,0 +1,9 @@
# guidedOnboardingExample
A Kibana plugin
---
## Development
See the [kibana contributing guide](https://github.com/elastic/kibana/blob/main/CONTRIBUTING.md) for instructions setting up your development environment.

View file

@ -0,0 +1,10 @@
/*
* 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.
*/
export const PLUGIN_ID = 'guidedOnboardingExample';
export const PLUGIN_NAME = 'guidedOnboardingExample';

View file

@ -0,0 +1,14 @@
{
"id": "guidedOnboardingExample",
"version": "1.0.0",
"kibanaVersion": "kibana",
"owner": {
"name": "platform-onboarding",
"githubTeam": "platform-onboarding"
},
"description": "Example plugin to consume guidedOnboarding",
"server": false,
"ui": true,
"requiredPlugins": ["navigation", "guidedOnboarding"],
"optionalPlugins": []
}

View file

@ -0,0 +1,30 @@
/*
* 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 from 'react';
import ReactDOM from 'react-dom';
import { AppMountParameters, CoreStart } from '@kbn/core/public';
import { AppPluginStartDependencies } from './types';
import { GuidedOnboardingExampleApp } from './components/app';
export const renderApp = (
{ notifications }: CoreStart,
{ guidedOnboarding }: AppPluginStartDependencies,
{ element, history }: AppMountParameters
) => {
ReactDOM.render(
<GuidedOnboardingExampleApp
notifications={notifications}
guidedOnboarding={guidedOnboarding}
history={history}
/>,
element
);
return () => ReactDOM.unmountComponentAtNode(element);
};

View file

@ -0,0 +1,70 @@
/*
* 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 from 'react';
import { FormattedMessage, I18nProvider } from '@kbn/i18n-react';
import { Router, Switch, Route } from 'react-router-dom';
import {
EuiPage,
EuiPageBody,
EuiPageContent_Deprecated as EuiPageContent,
EuiPageHeader,
EuiTitle,
} from '@elastic/eui';
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 { Main } from './main';
interface GuidedOnboardingExampleAppDeps {
notifications: CoreStart['notifications'];
guidedOnboarding: GuidedOnboardingPluginStart;
history: ScopedHistory;
}
export const GuidedOnboardingExampleApp = (props: GuidedOnboardingExampleAppDeps) => {
const { notifications, guidedOnboarding, history } = props;
return (
<I18nProvider>
<EuiPage restrictWidth="1000px">
<EuiPageBody>
<EuiPageHeader>
<EuiTitle size="l">
<h1>
<FormattedMessage
id="guidedOnboardingExample.title"
defaultMessage="Guided onboarding examples"
/>
</h1>
</EuiTitle>
</EuiPageHeader>
<EuiPageContent>
<Router history={history}>
<Switch>
<Route exact path="/">
<Main notifications={notifications} guidedOnboarding={guidedOnboarding} />
</Route>
<Route exact path="/stepOne">
<StepOne guidedOnboarding={guidedOnboarding} />
</Route>
<Route exact path="/stepTwo">
<StepTwo guidedOnboarding={guidedOnboarding} />
</Route>
</Switch>
</Router>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
</I18nProvider>
);
};

View file

@ -0,0 +1,255 @@
/*
* 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 { useHistory } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import {
EuiButton,
EuiFieldNumber,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiHorizontalRule,
EuiPageContentBody_Deprecated as EuiPageContentBody,
EuiPageContentHeader_Deprecated as EuiPageContentHeader,
EuiSelect,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import {
GuidedOnboardingPluginStart,
GuidedOnboardingState,
UseCase,
} from '@kbn/guided-onboarding-plugin/public';
import { CoreStart } from '@kbn/core/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 [selectedGuide, setSelectedGuide] = useState<
GuidedOnboardingState['activeGuide'] | undefined
>(undefined);
const [selectedStep, setSelectedStep] = useState<GuidedOnboardingState['activeStep'] | undefined>(
undefined
);
useEffect(() => {
const subscription = guidedOnboardingApi?.fetchGuideState$().subscribe((newState) => {
setGuideState(newState);
});
return () => subscription?.unsubscribe();
}, [guidedOnboardingApi]);
const startGuide = async (guide: UseCase) => {
const response = await guidedOnboardingApi?.updateGuideState({
activeGuide: guide,
activeStep: 'add_data',
});
if (response) {
notifications.toasts.addSuccess(
i18n.translate('guidedOnboardingExample.startGuide.toastLabel', {
defaultMessage: 'Guide (re-)started',
})
);
}
};
const updateGuideState = async () => {
const response = await guidedOnboardingApi?.updateGuideState({
activeGuide: selectedGuide!,
activeStep: selectedStep!,
});
if (response) {
notifications.toasts.addSuccess(
i18n.translate('guidedOnboardingExample.updateGuideState.toastLabel', {
defaultMessage: 'Guide state updated',
})
);
}
};
return (
<>
<EuiPageContentHeader>
<EuiTitle>
<h2>
<FormattedMessage
id="guidedOnboardingExample.main.title"
defaultMessage="Guided setup state"
/>
</h2>
</EuiTitle>
</EuiPageContentHeader>
<EuiPageContentBody>
<EuiText>
<h3>
<FormattedMessage
id="guidedOnboardingExample.main.currentStateTitle"
defaultMessage="Current state"
/>
</h3>
<p>
<FormattedMessage
id="guidedOnboardingExample.guidesSelection.state.explanation"
defaultMessage="The guide state on this page is updated automatically via an Observable,
so there is no need to 'load' the state from the server."
/>
</p>
{guideState ? (
<dl>
<dt>
<FormattedMessage
id="guidedOnboardingExample.guidesSelection.state.activeGuideLabel"
defaultMessage="Active guide"
/>
</dt>
<dd>{guideState.activeGuide ?? 'undefined'}</dd>
<dt>
<FormattedMessage
id="guidedOnboardingExample.guidesSelection.state.activeStepLabel"
defaultMessage="Active step"
/>
</dt>
<dd>{guideState.activeStep ?? 'undefined'}</dd>
</dl>
) : undefined}
</EuiText>
<EuiHorizontalRule />
<EuiText>
<h3>
<FormattedMessage
id="guidedOnboardingExample.main.startGuide.title"
defaultMessage="(Re-)Start a guide"
/>
</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>
</EuiFlexGroup>
<EuiSpacer />
<EuiHorizontalRule />
<EuiText>
<h3>
<FormattedMessage
id="guidedOnboardingExample.main.setGuideState.title"
defaultMessage="Set guide state"
/>
</h3>
</EuiText>
<EuiSpacer />
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormRow label="Guide" helpText="Select a guide">
<EuiSelect
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 shouldResetState = value.trim().length === 0;
if (shouldResetState) {
setSelectedGuide(undefined);
setSelectedStep(undefined);
} else {
setSelectedGuide(value);
}
}}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow label="Step">
<EuiFieldNumber
value={selectedStep}
onChange={(e) => setSelectedStep(e.target.value)}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFormRow hasEmptyLabelSpace>
<EuiButton onClick={updateGuideState}>Save</EuiButton>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<EuiHorizontalRule />
<EuiText>
<h3>
<FormattedMessage
id="guidedOnboardingExample.main.examplePages.title"
defaultMessage="Example pages"
/>
</h3>
</EuiText>
<EuiSpacer />
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton onClick={() => history.push('stepOne')}>
<FormattedMessage
id="guidedOnboardingExample.main.examplePages.stepOne.link"
defaultMessage="Step 1"
/>
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton onClick={() => history.push('stepTwo')}>
<FormattedMessage
id="guidedOnboardingExample.main.examplePages.stepTwo.link"
defaultMessage="Step 2"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageContentBody>
</>
);
};

View file

@ -0,0 +1,94 @@
/*
* 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 { FormattedMessage } from '@kbn/i18n-react';
import {
EuiButton,
EuiText,
EuiTourStep,
EuiTitle,
EuiPageContentHeader_Deprecated as EuiPageContentHeader,
EuiPageContentBody_Deprecated as EuiPageContentBody,
EuiSpacer,
} from '@elastic/eui';
import { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/public/types';
interface GuidedOnboardingExampleAppDeps {
guidedOnboarding: GuidedOnboardingPluginStart;
}
export const StepOne = ({ guidedOnboarding }: GuidedOnboardingExampleAppDeps) => {
const { guidedOnboardingApi } = guidedOnboarding;
const [isTourStepOpen, setIsTourStepOpen] = useState<boolean>(false);
useEffect(() => {
const subscription = guidedOnboardingApi?.fetchGuideState$().subscribe((newState) => {
const { activeGuide: guide, activeStep: step } = newState;
if (guide === 'search' && step === 'add_data') {
setIsTourStepOpen(true);
}
});
return () => subscription?.unsubscribe();
}, [guidedOnboardingApi]);
return (
<>
<EuiPageContentHeader>
<EuiTitle>
<h2>
<FormattedMessage
id="guidedOnboardingExample.stepOne.title"
defaultMessage="Example step Add data"
/>
</h2>
</EuiTitle>
</EuiPageContentHeader>
<EuiPageContentBody>
<EuiText>
<p>
<FormattedMessage
id="guidedOnboardingExample.guidesSelection.stepOne.explanation"
defaultMessage="The code on this page is listening to the guided setup state. If the state is set to
Search guide, step Add data, a EUI tour will be displayed, pointing to the button below. Alternatively,
the tour can be displayed via a localStorage value or a url param (see step 2)."
/>
</p>
</EuiText>
<EuiSpacer />
<EuiTourStep
content={
<EuiText>
<p>Click this button to complete step 1.</p>
</EuiText>
}
isStepOpen={isTourStepOpen}
minWidth={300}
onFinish={() => setIsTourStepOpen(false)}
step={1}
stepsTotal={1}
title="Step Add data"
anchorPosition="rightUp"
>
<EuiButton
onClick={async () => {
await guidedOnboardingApi?.updateGuideState({
activeGuide: 'search',
activeStep: 'search_experience',
});
}}
>
Complete step 1
</EuiButton>
</EuiTourStep>
</EuiPageContentBody>
</>
);
};

View file

@ -0,0 +1,95 @@
/*
* 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 { useHistory, useLocation } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiPageContentHeader_Deprecated as EuiPageContentHeader,
EuiPageContentBody_Deprecated as EuiPageContentBody,
} from '@elastic/eui';
interface StepTwoProps {
guidedOnboarding: GuidedOnboardingPluginStart;
}
export const StepTwo = (props: StepTwoProps) => {
const {
guidedOnboarding: { guidedOnboardingApi },
} = props;
const { search } = useLocation();
const history = useHistory();
const query = React.useMemo(() => new URLSearchParams(search), [search]);
useEffect(() => {
if (query.get('showTour') === 'true') {
setIsTourStepOpen(true);
}
}, [query]);
const [isTourStepOpen, setIsTourStepOpen] = useState<boolean>(false);
return (
<>
<EuiPageContentHeader>
<EuiTitle>
<h2>
<FormattedMessage
id="guidedOnboardingExample.stepTwo.title"
defaultMessage="Example step 2"
/>
</h2>
</EuiTitle>
</EuiPageContentHeader>
<EuiPageContentBody>
<EuiText>
<p>
<FormattedMessage
id="guidedOnboardingExample.guidesSelection.stepTwo.explanation"
defaultMessage="The EUI tour on this page is displayed, when a url param 'showTour' is set to 'true'."
/>
</p>
</EuiText>
<EuiSpacer />
<EuiTourStep
content={
<EuiText>
<p>Click this button to complete step 2.</p>
</EuiText>
}
isStepOpen={isTourStepOpen}
minWidth={300}
onFinish={() => {
history.push('/stepTwo');
query.set('showTour', 'false');
setIsTourStepOpen(false);
}}
step={1}
stepsTotal={1}
title="Step Add data"
anchorPosition="rightUp"
>
<EuiButton
onClick={async () => {
await guidedOnboardingApi?.updateGuideState({
activeGuide: 'search',
activeStep: 'optimize',
});
}}
>
Complete step 2
</EuiButton>
</EuiTourStep>
</EuiPageContentBody>
</>
);
};

View file

@ -0,0 +1,19 @@
/*
* 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 { GuidedOnboardingExamplePlugin } from './plugin';
// This exports static code and TypeScript types,
// as well as, Kibana Platform `plugin()` initializer.
export function plugin() {
return new GuidedOnboardingExamplePlugin();
}
export type {
GuidedOnboardingExamplePluginSetup,
GuidedOnboardingExamplePluginStart,
} from './types';

View file

@ -0,0 +1,43 @@
/*
* 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 { AppMountParameters, CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
import {
GuidedOnboardingExamplePluginSetup,
GuidedOnboardingExamplePluginStart,
AppPluginStartDependencies,
} from './types';
import { PLUGIN_NAME } from '../common';
export class GuidedOnboardingExamplePlugin
implements Plugin<GuidedOnboardingExamplePluginSetup, GuidedOnboardingExamplePluginStart>
{
public setup(core: CoreSetup): GuidedOnboardingExamplePluginSetup {
// Register an application into the side navigation menu
core.application.register({
id: 'guidedOnboardingExample',
title: PLUGIN_NAME,
async mount(params: AppMountParameters) {
// Load application bundle
const { renderApp } = await import('./application');
// Get start services as specified in kibana.json
const [coreStart, depsStart] = await core.getStartServices();
// Render the application
return renderApp(coreStart, depsStart as AppPluginStartDependencies, params);
},
});
return {};
}
public start(core: CoreStart): GuidedOnboardingExamplePluginStart {
return {};
}
public stop() {}
}

View file

@ -0,0 +1,21 @@
/*
* 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 { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public';
import { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/public/types';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface GuidedOnboardingExamplePluginSetup {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface GuidedOnboardingExamplePluginStart {}
export interface AppPluginStartDependencies {
navigation: NavigationPublicPluginStart;
guidedOnboarding: GuidedOnboardingPluginStart;
}

View file

@ -0,0 +1,24 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./target/types",
"emitDeclarationOnly": true,
"declaration": true,
"declarationMap": true
},
"include": [
"__jest__/**/*",
"common/**/*",
"public/**/*",
"server/**/*",
"../../typings/**/*",
],
"references": [
{
"path": "../../src/core/tsconfig.json"
},
{
"path": "../../src/plugins/guided_onboarding/tsconfig.json"
},
]
}