[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

@ -40,6 +40,7 @@
"eventAnnotation": "src/plugins/event_annotation",
"fieldFormats": "src/plugins/field_formats",
"flot": "packages/kbn-flot-charts/lib",
"guidedOnboarding": "src/plugins/guided_onboarding",
"home": "src/plugins/home",
"homePackages": "packages/home",
"indexPatternEditor": "src/plugins/data_view_editor",

View file

@ -176,6 +176,10 @@ for use in their own application.
|Index pattern fields formatters
|{kib-repo}blob/{branch}/src/plugins/guided_onboarding/README.md[guidedOnboarding]
|A Kibana plugin
|{kib-repo}blob/{branch}/src/plugins/home/README.md[home]
|Moves the legacy ui/registry/feature_catalogue module for registering "features" that should be shown in the home page's feature catalogue to a service within a "home" plugin. The feature catalogue refered to here should not be confused with the "feature" plugin for registering features used to derive UI capabilities for feature controls.

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"
},
]
}

View file

@ -56,6 +56,7 @@ pageLoadAssetSize:
globalSearchProviders: 25554
graph: 31504
grokdebugger: 26779
guidedOnboarding: 26875
home: 30182
indexLifecycleManagement: 107090
indexManagement: 140608

View file

@ -59,6 +59,7 @@ const previouslyRegisteredTypes = [
'fleet-enrollment-api-keys',
'fleet-preconfiguration-deletion-record',
'graph-workspace',
'guided-setup-state',
'index-pattern',
'infrastructure-monitoring-log-view',
'infrastructure-ui-source',

View file

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

View file

@ -0,0 +1,9 @@
# guidedOnboarding
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,12 @@
/*
* 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 = 'guidedOnboarding';
export const PLUGIN_NAME = 'guidedOnboarding';
export const API_BASE_PATH = '/api/guided_onboarding';

View file

@ -0,0 +1,15 @@
{
"id": "guidedOnboarding",
"version": "1.0.0",
"kibanaVersion": "kibana",
"owner": {
"name": "Journey Onboarding",
"githubTeam": "platform-onboarding"
},
"description": "Guided onboarding framework",
"server": true,
"ui": true,
"requiredBundles": ["kibanaReact"],
"optionalPlugins": [],
"configPath": ["guidedOnboarding"]
}

View file

@ -0,0 +1,9 @@
/*
* 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 { GuidedOnboardingButton } from './onboarding_button';

View file

@ -0,0 +1,241 @@
/*
* 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, { useState, useEffect, useRef } from 'react';
import { css } from '@emotion/react';
import {
EuiPopover,
EuiPopoverTitle,
EuiPopoverFooter,
EuiButton,
EuiText,
EuiProgress,
EuiAccordion,
EuiHorizontalRule,
EuiSpacer,
EuiTextColor,
htmlIdGenerator,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
useEuiTheme,
EuiButtonEmpty,
EuiTitle,
} from '@elastic/eui';
import { ApplicationStart } from '@kbn/core-application-browser';
import { HttpStart } from '@kbn/core-http-browser';
import { i18n } from '@kbn/i18n';
import { guidesConfig } from '../constants';
import type { GuideConfig, StepStatus, GuidedOnboardingState, StepConfig } from '../types';
import type { ApiService } from '../services/api';
interface Props {
api: ApiService;
application: ApplicationStart;
http: HttpStart;
}
const getConfig = (state?: GuidedOnboardingState): GuideConfig | undefined => {
if (state?.activeGuide && state.activeGuide !== 'unset') {
return guidesConfig[state.activeGuide];
}
return undefined;
};
const getStepLabel = (steps?: StepConfig[], state?: GuidedOnboardingState): string => {
if (steps && state?.activeStep) {
const activeStepIndex = steps.findIndex((step: StepConfig) => step.id === state.activeStep);
if (activeStepIndex > -1) {
return `: Step ${activeStepIndex + 1}`;
}
}
return '';
};
const getStepStatus = (steps: StepConfig[], stepIndex: number, activeStep?: string): StepStatus => {
const activeStepIndex = steps.findIndex((step: StepConfig) => step.id === activeStep);
if (activeStepIndex < stepIndex) {
return 'incomplete';
}
if (activeStepIndex === stepIndex) {
return 'in_progress';
}
return 'complete';
};
export const GuidedOnboardingButton = ({ api, application, http }: Props) => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const [guidedOnboardingState, setGuidedOnboardingState] = useState<
GuidedOnboardingState | undefined
>(undefined);
const firstRender = useRef(true);
useEffect(() => {
const subscription = api.fetchGuideState$().subscribe((newState) => {
if (
guidedOnboardingState?.activeGuide !== newState.activeGuide ||
guidedOnboardingState?.activeStep !== newState.activeStep
) {
if (firstRender.current) {
firstRender.current = false;
} else {
setIsPopoverOpen(true);
}
}
setGuidedOnboardingState(newState);
});
return () => subscription.unsubscribe();
}, [api, guidedOnboardingState?.activeGuide, guidedOnboardingState?.activeStep]);
const { euiTheme } = useEuiTheme();
const togglePopover = () => {
setIsPopoverOpen((prevIsPopoverOpen) => !prevIsPopoverOpen);
};
const popoverContainerCss = css`
width: 400px;
`;
const statusCircleCss = ({ status }: { status: StepStatus }) => css`
width: 24px;
height: 24px;
border-radius: 32px;
${(status === 'complete' || status === 'in_progress') &&
`background-color: ${euiTheme.colors.success};`}
${status === 'incomplete' &&
`
border: 2px solid ${euiTheme.colors.lightShade};
`}
`;
const guideConfig = getConfig(guidedOnboardingState);
const stepLabel = getStepLabel(guideConfig?.steps, guidedOnboardingState);
const navigateToStep = (step: StepConfig) => {
setIsPopoverOpen(false);
if (step.location) {
application.navigateToApp(step.location.appID, { path: step.location.path });
}
};
return guideConfig ? (
<EuiPopover
button={
<EuiButton onClick={togglePopover} color="success" fill>
{i18n.translate('guidedOnboarding.guidedSetupButtonLabel', {
defaultMessage: 'Guided setup{stepLabel}',
values: {
stepLabel,
},
})}
</EuiButton>
}
isOpen={isPopoverOpen}
closePopover={() => setIsPopoverOpen(false)}
anchorPosition="downRight"
hasArrow={false}
offset={10}
panelPaddingSize="l"
>
<EuiPopoverTitle>
<EuiButtonEmpty
onClick={() => {}}
iconSide="left"
iconType="arrowLeft"
isDisabled={true}
flush="left"
>
{i18n.translate('guidedOnboarding.dropdownPanel.backToGuidesLink', {
defaultMessage: 'Back to guides',
})}
</EuiButtonEmpty>
<EuiTitle size="m">
<h3>{guideConfig?.title}</h3>
</EuiTitle>
</EuiPopoverTitle>
<div css={popoverContainerCss}>
<EuiText>
<p>{guideConfig?.description}</p>
</EuiText>
<EuiSpacer />
<EuiHorizontalRule />
<EuiProgress label="Progress" value={40} max={100} size="l" valueText />
<EuiSpacer size="xl" />
{guideConfig?.steps.map((step, index, steps) => {
const accordionId = htmlIdGenerator(`accordion${index}`)();
const stepStatus = getStepStatus(steps, index, guidedOnboardingState?.activeStep);
const buttonContent = (
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<span css={statusCircleCss({ status: stepStatus })} className="eui-textCenter">
<span className="euiScreenReaderOnly">{stepStatus}</span>
{stepStatus === 'complete' && <EuiIcon type="check" color="white" />}
</span>
</EuiFlexItem>
<EuiFlexItem grow={false}>{step.title}</EuiFlexItem>
</EuiFlexGroup>
);
return (
<div>
<EuiAccordion
id={accordionId}
buttonContent={buttonContent}
arrowDisplay="right"
forceState={stepStatus === 'in_progress' ? 'open' : 'closed'}
>
<>
<EuiSpacer size="s" />
<EuiText size="s">{step.description}</EuiText>
<EuiSpacer />
{stepStatus === 'in_progress' && (
<EuiFlexGroup justifyContent="flexEnd">
<EuiFlexItem grow={false}>
<EuiButton onClick={() => navigateToStep(step)} fill>
{/* TODO: Support for conditional "Continue" button label if user revists a step */}
{i18n.translate('guidedOnboarding.dropdownPanel.startStepButtonLabel', {
defaultMessage: 'Start',
})}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
)}
</>
</EuiAccordion>
{/* Do not show horizontal rule for last item */}
{guideConfig.steps.length - 1 !== index && <EuiHorizontalRule margin="m" />}
</div>
);
})}
<EuiPopoverFooter>
<EuiText size="xs" textAlign="center">
<EuiTextColor color="subdued">
<p>
{i18n.translate('guidedOnboarding.dropdownPanel.footerDescription', {
defaultMessage: `Got questions? We're here to help.`,
})}
</p>
</EuiTextColor>
</EuiText>
</EuiPopoverFooter>
</div>
</EuiPopover>
) : (
<EuiButton onClick={togglePopover} color="success" fill isDisabled={true}>
Guided setup
</EuiButton>
);
};

View file

@ -0,0 +1,22 @@
/*
* 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 { securityConfig } from './security';
import { observabilityConfig } from './observability';
import { searchConfig } from './search';
import type { GuideConfig, UseCase } from '../types';
type GuidesConfig = {
[key in UseCase]: GuideConfig;
};
export const guidesConfig: GuidesConfig = {
security: securityConfig,
observability: observabilityConfig,
search: searchConfig,
};

View file

@ -0,0 +1,57 @@
/*
* 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 type { GuideConfig } from '../types';
export const observabilityConfig: GuideConfig = {
title: 'Observe my infrastructure',
description:
'The foundation of seeing Elastic in action, is adding you own data. Follow links to our documents below to learn more.',
docs: {
text: 'Observability 101 Documentation',
url: 'example.com',
},
steps: [
{
id: 'add_data',
title: 'Add data',
description:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam ligula enim, malesuada a finibus vel, cursus sed risus. Vivamus pretium, elit dictum lacinia aliquet, libero nibh dictum enim, a rhoncus leo magna in sapien.',
},
{
id: 'rules',
title: 'Customize your alerting rules',
description:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam ligula enim, malesuada a finibus vel, cursus sed risus. Vivamus pretium, elit dictum lacinia aliquet, libero nibh dictum enim, a rhoncus leo magna in sapien.',
},
{
id: 'infrastructure',
title: 'View infrastructure details',
description:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam ligula enim, malesuada a finibus vel, cursus sed risus. Vivamus pretium, elit dictum lacinia aliquet, libero nibh dictum enim, a rhoncus leo magna in sapien.',
},
{
id: 'explore',
title: 'Explore Discover and Dashboards',
description:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam ligula enim, malesuada a finibus vel, cursus sed risus. Vivamus pretium, elit dictum lacinia aliquet, libero nibh dictum enim, a rhoncus leo magna in sapien.',
},
{
id: 'tour',
title: 'Tour Observability',
description:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam ligula enim, malesuada a finibus vel, cursus sed risus. Vivamus pretium, elit dictum lacinia aliquet, libero nibh dictum enim, a rhoncus leo magna in sapien.',
},
{
id: 'do_more',
title: 'Do more with Observability',
description:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam ligula enim, malesuada a finibus vel, cursus sed risus. Vivamus pretium, elit dictum lacinia aliquet, libero nibh dictum enim, a rhoncus leo magna in sapien.',
},
],
};

View file

@ -0,0 +1,52 @@
/*
* 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 type { GuideConfig } from '../types';
export const searchConfig: GuideConfig = {
title: 'Search my data',
description: `We'll help you build world-class search experiences with your data.`,
docs: {
text: 'Enterprise Search 101 Documentation',
url: 'example.com',
},
steps: [
{
id: 'add_data',
title: 'Add data',
description:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam ligula enim, malesuada a finibus vel, cursus sed risus. Vivamus pretium, elit dictum lacinia aliquet, libero nibh dictum enim, a rhoncus leo magna in sapien.',
location: {
appID: 'guidedOnboardingExample',
path: 'stepOne',
},
},
{
id: 'search_experience',
title: 'Build a search experience',
description:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam ligula enim, malesuada a finibus vel, cursus sed risus. Vivamus pretium, elit dictum lacinia aliquet, libero nibh dictum enim, a rhoncus leo magna in sapien.',
location: {
appID: 'guidedOnboardingExample',
path: 'stepTwo?showTour=true',
},
},
{
id: 'optimize',
title: 'Optimize your search relevance',
description:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam ligula enim, malesuada a finibus vel, cursus sed risus. Vivamus pretium, elit dictum lacinia aliquet, libero nibh dictum enim, a rhoncus leo magna in sapien.',
},
{
id: 'review',
title: 'Review your search analytics',
description:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam ligula enim, malesuada a finibus vel, cursus sed risus. Vivamus pretium, elit dictum lacinia aliquet, libero nibh dictum enim, a rhoncus leo magna in sapien.',
},
],
};

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 type { GuideConfig } from '../types';
export const securityConfig: GuideConfig = {
title: 'Get started with SIEM',
description:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam ligula enim, malesuada a finibus vel, cursus sed risus. Vivamus pretium, elit dictum lacinia aliquet, libero nibh dictum enim, a rhoncus leo magna in sapien.',
steps: [
{
id: 'add_data',
title: 'Add and view your data',
description:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam ligula enim, malesuada a finibus vel, cursus sed risus. Vivamus pretium, elit dictum lacinia aliquet, libero nibh dictum enim, a rhoncus leo magna in sapien.',
},
{
id: 'rules',
title: 'Turn on rules',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
},
{
id: 'alerts',
title: 'View Alerts',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
},
{
id: 'cases',
title: 'Cases and investigations',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
},
{
id: 'do_more',
title: 'Do more with Elastic Security',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
},
],
};

View file

@ -0,0 +1,20 @@
/*
* 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 { PluginInitializerContext } from '@kbn/core/public';
import { GuidedOnboardingPlugin } from './plugin';
export function plugin(ctx: PluginInitializerContext) {
return new GuidedOnboardingPlugin(ctx);
}
export type {
GuidedOnboardingPluginSetup,
GuidedOnboardingPluginStart,
GuidedOnboardingState,
UseCase,
} from './types';

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 ReactDOM from 'react-dom';
import React from 'react';
import * as Rx from 'rxjs';
import { I18nProvider } from '@kbn/i18n-react';
import {
CoreSetup,
CoreStart,
Plugin,
CoreTheme,
ApplicationStart,
HttpStart,
PluginInitializerContext,
} from '@kbn/core/public';
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import {
ClientConfigType,
GuidedOnboardingPluginSetup,
GuidedOnboardingPluginStart,
} from './types';
import { GuidedOnboardingButton } from './components';
import { ApiService, apiService } from './services/api';
export class GuidedOnboardingPlugin
implements Plugin<GuidedOnboardingPluginSetup, GuidedOnboardingPluginStart>
{
constructor(private ctx: PluginInitializerContext) {}
public setup(core: CoreSetup): GuidedOnboardingPluginSetup {
return {};
}
public start(core: CoreStart): GuidedOnboardingPluginStart {
const { ui: isGuidedOnboardingUiEnabled } = this.ctx.config.get<ClientConfigType>();
if (!isGuidedOnboardingUiEnabled) {
return {};
}
const { chrome, http, theme, application } = core;
// Initialize services
apiService.setup(http);
chrome.navControls.registerExtension({
order: 1000,
mount: (target) =>
this.mount({
targetDomElement: target,
theme$: theme.theme$,
api: apiService,
application,
http,
}),
});
// Return methods that should be available to other plugins
return {
guidedOnboardingApi: apiService,
};
}
public stop() {}
private mount({
targetDomElement,
theme$,
api,
application,
http,
}: {
targetDomElement: HTMLElement;
theme$: Rx.Observable<CoreTheme>;
api: ApiService;
application: ApplicationStart;
http: HttpStart;
}) {
ReactDOM.render(
<KibanaThemeProvider theme$={theme$}>
<I18nProvider>
<GuidedOnboardingButton api={api} application={application} http={http} />
</I18nProvider>
</KibanaThemeProvider>,
targetDomElement
);
return () => ReactDOM.unmountComponentAtNode(targetDomElement);
}
}

View file

@ -0,0 +1,59 @@
/*
* 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 { HttpSetup } from '@kbn/core/public';
import { BehaviorSubject, map, from, concatMap, of } from 'rxjs';
import { API_BASE_PATH } from '../../common';
import { GuidedOnboardingState } from '../types';
export class ApiService {
private client: HttpSetup | undefined;
private onboardingGuideState$!: BehaviorSubject<GuidedOnboardingState | undefined>;
public setup(httpClient: HttpSetup): void {
this.client = httpClient;
this.onboardingGuideState$ = new BehaviorSubject<GuidedOnboardingState | undefined>(undefined);
}
public fetchGuideState$() {
// TODO add error handling if this.client has not been initialized or request fails
return this.onboardingGuideState$.pipe(
concatMap((state) =>
state === undefined
? from(this.client!.get<{ state: GuidedOnboardingState }>(`${API_BASE_PATH}/state`)).pipe(
map((response) => response.state)
)
: of(state)
)
);
}
public async updateGuideState(newState: GuidedOnboardingState) {
if (!this.client) {
throw new Error('ApiService has not be initialized.');
}
try {
const response = await this.client.put<{ state: GuidedOnboardingState }>(
`${API_BASE_PATH}/state`,
{
body: JSON.stringify(newState),
}
);
this.onboardingGuideState$.next(newState);
return response;
} catch (error) {
// TODO handle error
// eslint-disable-next-line no-console
console.error(error);
}
}
}
export const apiService = new ApiService();

View file

@ -0,0 +1,54 @@
/*
* 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 { ApiService } from './services/api';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface GuidedOnboardingPluginSetup {}
export interface GuidedOnboardingPluginStart {
guidedOnboardingApi?: ApiService;
}
export interface AppPluginStartDependencies {
navigation: NavigationPublicPluginStart;
}
export type UseCase = 'observability' | 'security' | 'search';
export type StepStatus = 'incomplete' | 'complete' | 'in_progress';
export interface StepConfig {
id: string;
title: string;
description: string;
location?: {
appID: string;
path: string;
};
status?: StepStatus;
}
export interface GuideConfig {
title: string;
description: string;
docs?: {
text: string;
url: string;
};
steps: StepConfig[];
}
export interface GuidedOnboardingState {
activeGuide: UseCase | 'unset';
activeStep: string | 'unset';
}
export interface ClientConfigType {
ui: boolean;
}

View file

@ -0,0 +1,25 @@
/*
* 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 { PluginConfigDescriptor } from '@kbn/core/server';
import { schema, TypeOf } from '@kbn/config-schema';
// By default, hide any guided onboarding UI. Change it with guidedOnboarding.ui:true in kibana.dev.yml
const configSchema = schema.object({
ui: schema.boolean({ defaultValue: false }),
});
export type GuidedOnboardingConfig = TypeOf<typeof configSchema>;
export const config: PluginConfigDescriptor<GuidedOnboardingConfig> = {
// define which config properties should be available in the client side plugin
exposeToBrowser: {
ui: true,
},
schema: configSchema,
};

View file

@ -0,0 +1,18 @@
/*
* 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 { PluginInitializerContext } from '@kbn/core/server';
import { GuidedOnboardingPlugin } from './plugin';
export { config } from './config';
export function plugin(initializerContext: PluginInitializerContext) {
return new GuidedOnboardingPlugin(initializerContext);
}
export type { GuidedOnboardingPluginSetup, GuidedOnboardingPluginStart } 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 { PluginInitializerContext, CoreSetup, Plugin, Logger } from '@kbn/core/server';
import { GuidedOnboardingPluginSetup, GuidedOnboardingPluginStart } from './types';
import { defineRoutes } from './routes';
import { guidedSetupSavedObjects } from './saved_objects';
export class GuidedOnboardingPlugin
implements Plugin<GuidedOnboardingPluginSetup, GuidedOnboardingPluginStart>
{
private readonly logger: Logger;
constructor(initializerContext: PluginInitializerContext) {
this.logger = initializerContext.logger.get();
}
public setup(core: CoreSetup) {
this.logger.debug('guidedOnboarding: Setup');
const router = core.http.createRouter();
// Register server side APIs
defineRoutes(router);
// register saved objects
core.savedObjects.registerType(guidedSetupSavedObjects);
return {};
}
public start() {
this.logger.debug('guidedOnboarding: Started');
return {};
}
public stop() {}
}

View file

@ -0,0 +1,101 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { IRouter, SavedObjectsClient } from '@kbn/core/server';
import {
guidedSetupDefaultState,
guidedSetupSavedObjectsId,
guidedSetupSavedObjectsType,
} from '../saved_objects';
const doesGuidedSetupExist = async (savedObjectsClient: SavedObjectsClient): Promise<boolean> => {
return savedObjectsClient
.find({ type: guidedSetupSavedObjectsType })
.then((foundSavedObjects) => foundSavedObjects.total > 0);
};
export function defineRoutes(router: IRouter) {
router.get(
{
path: '/api/guided_onboarding/state',
validate: false,
},
async (context, request, response) => {
const coreContext = await context.core;
const soClient = coreContext.savedObjects.client as SavedObjectsClient;
const stateExists = await doesGuidedSetupExist(soClient);
if (stateExists) {
const guidedSetupSO = await soClient.get(
guidedSetupSavedObjectsType,
guidedSetupSavedObjectsId
);
return response.ok({
body: { state: guidedSetupSO.attributes },
});
} else {
return response.ok({
body: { state: guidedSetupDefaultState },
});
}
}
);
router.put(
{
path: '/api/guided_onboarding/state',
validate: {
body: schema.object({
activeGuide: schema.maybe(schema.string()),
activeStep: schema.maybe(schema.string()),
}),
},
},
async (context, request, response) => {
const activeGuide = request.body.activeGuide;
const activeStep = request.body.activeStep;
const attributes = {
activeGuide: activeGuide ?? 'unset',
activeStep: activeStep ?? 'unset',
};
const coreContext = await context.core;
const soClient = coreContext.savedObjects.client as SavedObjectsClient;
const stateExists = await doesGuidedSetupExist(soClient);
if (stateExists) {
const updatedGuidedSetupSO = await soClient.update(
guidedSetupSavedObjectsType,
guidedSetupSavedObjectsId,
attributes
);
return response.ok({
body: { state: updatedGuidedSetupSO.attributes },
});
} else {
const guidedSetupSO = await soClient.create(
guidedSetupSavedObjectsType,
{
...guidedSetupDefaultState,
...attributes,
},
{
id: guidedSetupSavedObjectsId,
}
);
return response.ok({
body: {
state: guidedSetupSO.attributes,
},
});
}
}
);
}

View file

@ -0,0 +1,33 @@
/*
* 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 { SavedObjectsType } from '@kbn/core/server';
export const guidedSetupSavedObjectsType = 'guided-setup-state';
export const guidedSetupSavedObjectsId = 'guided-setup-state-id';
export const guidedSetupDefaultState = {
activeGuide: 'unset',
activeStep: 'unset',
};
export const guidedSetupSavedObjects: SavedObjectsType = {
name: guidedSetupSavedObjectsType,
hidden: false,
// make it available in all spaces for now
namespaceType: 'agnostic',
mappings: {
dynamic: false,
properties: {
activeGuide: {
type: 'keyword',
},
activeStep: {
type: 'keyword',
},
},
},
};

View file

@ -0,0 +1,14 @@
/*
* 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 {
guidedSetupSavedObjects,
guidedSetupSavedObjectsType,
guidedSetupSavedObjectsId,
guidedSetupDefaultState,
} from './guided_setup';

View file

@ -0,0 +1,13 @@
/*
* 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.
*/
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface GuidedOnboardingPluginSetup {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface GuidedOnboardingPluginStart {}

View file

@ -0,0 +1,22 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./target/types",
"emitDeclarationOnly": true,
"declaration": true,
"declarationMap": true
},
"include": ["common/**/*", "public/**/*", "server/**/*"],
"references": [
{
"path": "../../core/tsconfig.json"
},
{
"path": "../navigation/tsconfig.json"
},
{
"path": "../kibana_react/tsconfig.json"
},
]
}

View file

@ -106,6 +106,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) {
'data.search.sessions.pageSize (number)',
'data.search.sessions.trackingInterval (duration)',
'enterpriseSearch.host (string)',
'guidedOnboarding.ui (boolean)',
'home.disableWelcomeScreen (boolean)',
'map.emsFileApiUrl (string)',
'map.emsFontLibraryUrl (string)',

View file

@ -21,6 +21,8 @@
"@kbn/expressions-explorer-plugin/*": ["examples/expressions_explorer/*"],
"@kbn/field-formats-example-plugin": ["examples/field_formats_example"],
"@kbn/field-formats-example-plugin/*": ["examples/field_formats_example/*"],
"@kbn/guided-onboarding-example-plugin": ["examples/guided_onboarding_example"],
"@kbn/guided-onboarding-example-plugin/*": ["examples/guided_onboarding_example/*"],
"@kbn/hello-world-plugin": ["examples/hello_world"],
"@kbn/hello-world-plugin/*": ["examples/hello_world/*"],
"@kbn/locator-examples-plugin": ["examples/locator_examples"],
@ -113,6 +115,8 @@
"@kbn/expressions-plugin/*": ["src/plugins/expressions/*"],
"@kbn/field-formats-plugin": ["src/plugins/field_formats"],
"@kbn/field-formats-plugin/*": ["src/plugins/field_formats/*"],
"@kbn/guided-onboarding-plugin": ["src/plugins/guided_onboarding"],
"@kbn/guided-onboarding-plugin/*": ["src/plugins/guided_onboarding/*"],
"@kbn/home-plugin": ["src/plugins/home"],
"@kbn/home-plugin/*": ["src/plugins/home/*"],
"@kbn/input-control-vis-plugin": ["src/plugins/input_control_vis"],