[Guided onboarding] Dynamic URLs for steps (#154572)

## Summary
Fixes https://github.com/elastic/kibana/issues/143322

This PR adds an option to store dynamic parameters when a step is
completed to be used later for dynamically built URLs.

### How to test
1. Add `xpack.cloud.id: 'testID'` to your `/config/kibana.dev.yml` file
2. Start ES with `yarn es snapshot`
3. Start Kibana with `yarn start --run-examples` 
4. Navigate to the guided onboarding example plugin
`http://localhost:5601/app/guidedOnboardingExample`
5. Start the test guide and when completing step 1 provide any value for
the paramter `indexName`
6. Continue the guide until step 4 and check that the correct value is
being used for the url of this step.

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Yulia Čech 2023-04-21 18:21:29 +02:00 committed by GitHub
parent 89258d7a1c
commit bc3471cf26
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 403 additions and 39 deletions

View file

@ -25,6 +25,7 @@ import { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/publi
import { StepTwo } from './step_two';
import { StepOne } from './step_one';
import { StepThree } from './step_three';
import { StepFour } from './step_four';
import { Main } from './main';
interface GuidedOnboardingExampleAppDeps {
@ -65,6 +66,13 @@ export const GuidedOnboardingExampleApp = (props: GuidedOnboardingExampleAppDeps
<Route exact path="/stepThree">
<StepThree guidedOnboarding={guidedOnboarding} />
</Route>
p
<Route
path="/stepFour/:indexName?"
render={(routeProps) => (
<StepFour guidedOnboarding={guidedOnboarding} {...routeProps} />
)}
/>
</Switch>
</Router>
</EuiPageContent>

View file

@ -345,6 +345,14 @@ export const Main = (props: MainProps) => {
/>
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton onClick={() => history.push('stepFour')}>
<FormattedMessage
id="guidedOnboardingExample.main.examplePages.stepFour.link"
defaultMessage="Step 4"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageContentBody>
</>

View file

@ -0,0 +1,83 @@
/*
* 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 } 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,
EuiCode,
} from '@elastic/eui';
import { RouteComponentProps } from 'react-router-dom';
interface StepFourProps {
guidedOnboarding: GuidedOnboardingPluginStart;
}
export const StepFour = (props: StepFourProps & RouteComponentProps<{ indexName: string }>) => {
const {
guidedOnboarding: { guidedOnboardingApi },
match: {
params: { indexName },
},
} = props;
const [, setIsTourStepOpen] = useState<boolean>(false);
useEffect(() => {
const subscription = guidedOnboardingApi
?.isGuideStepActive$('testGuide', 'step4')
.subscribe((isStepActive) => {
setIsTourStepOpen(isStepActive);
});
return () => subscription?.unsubscribe();
}, [guidedOnboardingApi]);
return (
<>
<EuiPageContentHeader>
<EuiTitle>
<h2>
<FormattedMessage
id="guidedOnboardingExample.stepFour.title"
defaultMessage="Example step 4"
/>
</h2>
</EuiTitle>
</EuiPageContentHeader>
<EuiPageContentBody>
<EuiText>
<p>
<FormattedMessage
id="guidedOnboardingExample.guidesSelection.stepFour.explanation"
defaultMessage="This step has a dynamic URL with a param {indexName} passed in step 1"
values={{
indexName: (
<EuiCode language="javascript">&#123;indexName: {indexName}&#125;</EuiCode>
),
}}
/>
</p>
</EuiText>
<EuiSpacer />
<EuiButton
onClick={async () => {
await guidedOnboardingApi?.completeGuideStep('testGuide', 'step4');
}}
>
Complete step 4
</EuiButton>
</EuiPageContentBody>
</>
);
};

View file

@ -16,6 +16,11 @@ import {
EuiPageContentHeader_Deprecated as EuiPageContentHeader,
EuiPageContentBody_Deprecated as EuiPageContentBody,
EuiSpacer,
EuiCode,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
} from '@elastic/eui';
import useObservable from 'react-use/lib/useObservable';
@ -30,6 +35,7 @@ export const StepOne = ({ guidedOnboarding }: GuidedOnboardingExampleAppDeps) =>
const { guidedOnboardingApi } = guidedOnboarding;
const [isTourStepOpen, setIsTourStepOpen] = useState<boolean>(false);
const [indexName, setIndexName] = useState('test1234');
const isTourActive = useObservable(
guidedOnboardingApi!.isGuideStepActive$('testGuide', 'step1'),
@ -59,30 +65,59 @@ export const StepOne = ({ guidedOnboarding }: GuidedOnboardingExampleAppDeps) =>
Test guide, step 1, a EUI tour will be displayed, pointing to the button below."
/>
</p>
<p>
<FormattedMessage
id="guidedOnboardingExample.guidesSelection.stepOne.dynamicParamsExplanation"
defaultMessage="There is also an input field to provide a dynamic parameter {indexName} for step 4."
values={{
indexName: <EuiCode language="javascript">indexName</EuiCode>,
}}
/>
</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 1"
anchorPosition="rightUp"
>
<EuiButton
onClick={async () => {
await guidedOnboardingApi?.completeGuideStep('testGuide', 'step1');
}}
>
Complete step 1
</EuiButton>
</EuiTourStep>
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormRow
label={
<FormattedMessage
id="guidedOnboardingExample.guidesSelection.stepOne.indexNameInputLabel"
defaultMessage="indexName"
/>
}
>
<EuiFieldText value={indexName} onChange={(e) => setIndexName(e.target.value)} />
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow hasEmptyLabelSpace>
<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 1"
anchorPosition="rightUp"
>
<EuiButton
onClick={async () => {
await guidedOnboardingApi?.completeGuideStep('testGuide', 'step1', {
indexName,
});
}}
>
Complete step 1
</EuiButton>
</EuiTourStep>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageContentBody>
</>
);

View file

@ -16,6 +16,7 @@ export type {
GuideConfig,
StepConfig,
StepDescriptionWithLink,
GuideParams,
} from './src/types';
export { GuideCards, GuideFilters } from './src/components/landing_page';
export type { GuideFilterValues } from './src/components/landing_page';

View file

@ -75,5 +75,14 @@ export const testGuideConfig: GuideConfig = {
path: 'stepThree',
},
},
{
id: 'step4',
title: 'Step 4 (dynamic url)',
description: 'This step navigates to a dynamic URL with a param indexName passed in step 1.',
location: {
appID: 'guidedOnboardingExample',
path: 'stepFour/{indexName}',
},
},
],
};

View file

@ -17,15 +17,18 @@ export type GuideId =
type KubernetesStepIds = 'add_data' | 'view_dashboard' | 'tour_observability';
type SiemStepIds = 'add_data' | 'rules' | 'alertsCases';
type SearchStepIds = 'add_data' | 'search_experience';
type TestGuideIds = 'step1' | 'step2' | 'step3';
type TestGuideIds = 'step1' | 'step2' | 'step3' | 'step4';
export type GuideStepIds = KubernetesStepIds | SiemStepIds | SearchStepIds | TestGuideIds;
export type GuideParams = Record<string, string>;
export interface GuideState {
guideId: GuideId;
status: GuideStatus;
isActive?: boolean; // Drives the current guide shown in the dropdown panel
steps: GuideStep[];
params?: GuideParams;
}
/**
@ -92,6 +95,16 @@ export interface StepConfig {
description?: string | StepDescriptionWithLink;
// description list is displayed as an unordered list, can be combined with description
descriptionList?: Array<string | StepDescriptionWithLink>;
/*
* Kibana location where the user will be redirected when starting or continuing a guide step.
* The property `path` can use dynamic parameters, for example `testPath/{indexID}/{pageID}.
* For the dynamic path to be configured correctly, the values of the parameters need to be passed to
* the api service when completing one of the previous steps.
* For example, if step 2 has a dynamic parameter `indexID` in its location path
* { appID: 'test', path: 'testPath/{indexID}', params: ['indexID'] },
* its value needs to be passed to the api service when completing step 1. For example,
* `guidedOnboardingAPI.completeGuideStep('testGuide', 'step1', { indexID: 'testIndex' })
*/
location?: {
appID: string;
path: string;

View file

@ -45,18 +45,18 @@ When starting Kibana with `yarn start --run-examples` the `guided_onboarding_exa
The guided onboarding plugin exposes an API service from its start contract that is intended to be used by other plugins. The API service allows consumers to access the current state of the guided onboarding process and manipulate it.
To use the API service in your plugin, declare the guided onboarding plugin as a dependency in the file `kibana.json` of your plugin. Add the API service to your plugin's start dependencies to rely on the provided TypeScript interface:
```
```js
export interface AppPluginStartDependencies {
guidedOnboarding: GuidedOnboardingPluginStart;
}
```
The API service is now available to your plugin in the setup lifecycle function of your plugin
```
```js
// startDependencies is of type AppPluginStartDependencies
const [coreStart, startDependencies] = await core.getStartServices();
```
or in the start lifecycle function of your plugin.
```
```js
public start(core: CoreStart, startDependencies: AppPluginStartDependencies) {
...
}
@ -67,7 +67,7 @@ public start(core: CoreStart, startDependencies: AppPluginStartDependencies) {
The API service exposes an Observable that contains a boolean value for the state of a specific guide step. For example, if your plugin needs to check if the "Add data" step of the SIEM guide is currently active, you could use the following code snippet.
```
```js
const { guidedOnboardingApi } = guidedOnboarding;
const isDataStepActive = useObservable(guidedOnboardingApi!.isGuideStepActive$('siem', 'add_data'));
useEffect(() => {
@ -76,7 +76,7 @@ useEffect(() => {
```
Alternatively, you can subscribe to the Observable directly.
```
```js
useEffect(() => {
const subscription = guidedOnboardingApi?.isGuideStepActive$('siem', 'add_data').subscribe((isDataStepACtive) => {
// do some logic depending on the step state
@ -89,7 +89,7 @@ useEffect(() => {
Similar to `isGuideStepActive$`, the observable `isGuideStepReadyToComplete$` can be used to track the state of a step that is configured for manual completion. The observable broadcasts `true` when the manual completion popover is displayed and the user can mark the step "done". In this state the step is not in progress anymore but is not yet fully completed.
### completeGuideStep(guideId: GuideId, stepId: GuideStepIds): Promise\<{ pluginState: PluginState } | undefined\>
### completeGuideStep(guideId: GuideId, stepId: GuideStepIds, params?: GuideParams): Promise\<{ pluginState: PluginState } | undefined\>
The API service exposes an async function to mark a guide step as completed.
If the specified guide step is not currently active, the function is a noop. In that case the return value is `undefined`,
otherwise an updated `PluginState` is returned.
@ -98,8 +98,20 @@ otherwise an updated `PluginState` is returned.
await guidedOnboardingApi?.completeGuideStep('siem', 'add_data');
```
The function also accepts an optional argument `params` that will be saved in the state and later used for step URLs with dynamic parameters. For example, step 2 of the guide has a dynamic parameter `indexID` in its location path:
```js
const step2Config = {
id: 'step2',
description: 'Step with dynamic url',
location: {
appID: 'test', path: 'testPath/{indexID}'
}
};
```
The value of the parameter `indexID` needs to be passed to the API service when completing step 1: `completeGuideStep('testGuide', 'step1', { indexID: 'testIndex' })`
## Guides config
To use the API service, you need to know a guide ID (currently one of `search`, `kubernetes`, `siem`) and a step ID (for example, `add_data`, `search_experience`, `rules` etc). The consumers of guided onboarding register their guide configs themselves and have therefore full control over the guide ID and step IDs used for their guide. For more details on registering a guide config, see below.
To use the API service, you need to know a guide ID (currently one of `appSearch`, `websiteSearch`, `databaseSearch`, `kubernetes`, `siem`) and a step ID (for example, `add_data`, `search_experience`, `rules` etc). The consumers of guided onboarding register their guide configs themselves and have therefore full control over the guide ID and step IDs used for their guide. For more details on registering a guide config, see below.
## Server side: register a guide config
The guided onboarding exposes a function `registerGuideConfig(guideId: GuideId, guideConfig: GuideConfig)` function in its setup contract. This function allows consumers to register a guide config for a specified guide ID. The function throws an error if a config already exists for the guide ID. See code examples in following plugins:

View file

@ -0,0 +1,47 @@
/*
* 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 { PluginState } from '../../common';
import { testGuideStep4ActiveState } from '../services/api.mocks';
import { getStepLocationPath } from './get_step_location';
describe('getStepLocationPath', () => {
let result: string | undefined;
const pathWithParams = 'testPath/{param1}/{param2}';
const pathWithoutParams = 'testPath';
const pluginStateWithoutParams: PluginState = {
status: 'in_progress',
isActivePeriod: true,
activeGuide: testGuideStep4ActiveState,
};
it('returns initial location path if no params passed', () => {
result = getStepLocationPath(pathWithParams, pluginStateWithoutParams);
expect(result).toBe(pathWithParams);
});
it('returns dynamic location path if params passed', () => {
const pluginStateWithParams: PluginState = {
status: 'in_progress',
isActivePeriod: true,
activeGuide: { ...testGuideStep4ActiveState, params: { param1: 'test1', param2: 'test2' } },
};
result = getStepLocationPath(pathWithParams, pluginStateWithParams);
expect(result).toBe(`testPath/test1/test2`);
});
it('returns initial location path if params passed but no params are used in the location', () => {
const pluginStateWithParams: PluginState = {
status: 'in_progress',
isActivePeriod: true,
activeGuide: { ...testGuideStep4ActiveState, params: { indexName: 'test1234' } },
};
result = getStepLocationPath(pathWithoutParams, pluginStateWithParams);
expect(result).toBe(`testPath`);
});
});

View file

@ -0,0 +1,26 @@
/*
* 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 { PluginState } from '../../common';
// regex matches everything between an opening and a closing curly braces
// without matching the braces themselves
const paramsBetweenCurlyBraces = /(?<=\{)[^\{\}]+(?=\})/g;
export const getStepLocationPath = (path: string, pluginState: PluginState): string | undefined => {
if (pluginState.activeGuide?.params) {
let dynamicPath = path;
const matchedParams = path.match(paramsBetweenCurlyBraces);
if (matchedParams) {
for (const param of matchedParams) {
dynamicPath = dynamicPath.replace(`{${param}}`, pluginState.activeGuide?.params[param]);
}
return dynamicPath;
}
}
return path;
};

View file

@ -457,7 +457,7 @@ describe('Guided setup', () => {
expect(
find('guidePanelStepDescription')
.last()
.first()
.containsMatchingElement(<p>{testGuideConfig.steps[2].description}</p>)
).toBe(true);
});

View file

@ -24,6 +24,7 @@ import { getGuidePanelStyles } from './guide_panel.styles';
import { GuideButton } from './guide_button';
import { GuidePanelFlyout } from './guide_panel_flyout';
import { getStepLocationPath } from './get_step_location';
interface GuidePanelProps {
api: GuidedOnboardingApi;
@ -76,7 +77,7 @@ export const GuidePanel = ({ api, application, notifications, uiSettings }: Guid
if (stepConfig.location) {
await application.navigateToApp(stepConfig.location.appID, {
path: stepConfig.location.path,
path: getStepLocationPath(stepConfig.location.path, pluginState),
});
if (stepConfig.manualCompletion?.readyToCompleteOnNavigation) {

View file

@ -12,7 +12,7 @@ import { PluginState } from '../../common';
export const testGuideFirstStep: GuideStepIds = 'step1';
export const testGuideManualCompletionStep = 'step2';
export const testGuideLastStep: GuideStepIds = 'step3';
export const testGuideLastStep: GuideStepIds = 'step4';
export const testIntegration = 'testIntegration';
export const wrongIntegration = 'notTestIntegration';
@ -33,6 +33,10 @@ export const testGuideStep1ActiveState: GuideState = {
id: 'step3',
status: 'inactive',
},
{
id: 'step4',
status: 'inactive',
},
],
};
@ -45,6 +49,7 @@ export const testGuideStep1InProgressState: GuideState = {
},
testGuideStep1ActiveState.steps[1],
testGuideStep1ActiveState.steps[2],
testGuideStep1ActiveState.steps[3],
],
};
@ -60,6 +65,7 @@ export const testGuideStep2ActiveState: GuideState = {
status: 'active',
},
testGuideStep1ActiveState.steps[2],
testGuideStep1ActiveState.steps[3],
],
};
@ -75,6 +81,7 @@ export const testGuideStep2InProgressState: GuideState = {
status: 'in_progress',
},
testGuideStep1ActiveState.steps[2],
testGuideStep1ActiveState.steps[3],
],
};
@ -90,6 +97,7 @@ export const testGuideStep2ReadyToCompleteState: GuideState = {
status: 'ready_to_complete',
},
testGuideStep1ActiveState.steps[2],
testGuideStep1ActiveState.steps[3],
],
};
@ -108,6 +116,29 @@ export const testGuideStep3ActiveState: GuideState = {
id: testGuideStep1ActiveState.steps[2].id,
status: 'active',
},
testGuideStep1ActiveState.steps[3],
],
};
export const testGuideStep4ActiveState: GuideState = {
...testGuideStep1ActiveState,
steps: [
{
...testGuideStep1ActiveState.steps[0],
status: 'complete',
},
{
id: testGuideStep1ActiveState.steps[1].id,
status: 'complete',
},
{
id: testGuideStep1ActiveState.steps[2].id,
status: 'complete',
},
{
id: testGuideStep1ActiveState.steps[3].id,
status: 'active',
},
],
};
@ -126,6 +157,10 @@ export const readyToCompleteGuideState: GuideState = {
...testGuideStep1ActiveState.steps[2],
status: 'complete',
},
{
...testGuideStep1ActiveState.steps[3],
status: 'complete',
},
],
};
@ -144,3 +179,8 @@ export const mockPluginStateInProgress: PluginState = {
isActivePeriod: true,
activeGuide: testGuideStep1ActiveState,
};
export const testGuideParams = {
param1: 'test1',
param2: 'test2',
};

View file

@ -16,7 +16,6 @@ import { API_BASE_PATH } from '../../common';
import { ApiService } from './api.service';
import {
testGuideFirstStep,
testGuideLastStep,
testGuideManualCompletionStep,
testGuideStep1ActiveState,
testGuideStep1InProgressState,
@ -30,6 +29,7 @@ import {
mockPluginStateNotStarted,
testGuideStep3ActiveState,
testGuideStep2ReadyToCompleteState,
testGuideParams,
} from './api.mocks';
describe('GuidedOnboarding ApiService', () => {
@ -395,6 +395,21 @@ describe('GuidedOnboarding ApiService', () => {
});
});
it(`saves the params if present`, async () => {
httpClient.get.mockResolvedValue({
pluginState: { ...mockPluginStateInProgress, activeGuide: testGuideStep1InProgressState },
});
apiService.setup(httpClient, true);
await apiService.completeGuideStep(testGuideId, testGuideFirstStep, testGuideParams);
expect(httpClient.put).toHaveBeenCalledTimes(1);
// Verify the params were sent to the endpoint
expect(httpClient.put).toHaveBeenLastCalledWith(`${API_BASE_PATH}/state`, {
body: JSON.stringify({ guide: { ...testGuideStep2ActiveState, params: testGuideParams } }),
});
});
it(`marks the step as 'ready_to_complete' if it's configured for manual completion`, async () => {
httpClient.get.mockResolvedValueOnce({
pluginState: { ...mockPluginStateInProgress, activeGuide: testGuideStep2InProgressState },
@ -416,6 +431,7 @@ describe('GuidedOnboarding ApiService', () => {
testGuideStep2InProgressState.steps[0],
{ ...testGuideStep2InProgressState.steps[1], status: 'ready_to_complete' },
testGuideStep2InProgressState.steps[2],
testGuideStep2InProgressState.steps[3],
],
},
}),
@ -436,11 +452,21 @@ describe('GuidedOnboarding ApiService', () => {
},
});
httpClient.get.mockResolvedValueOnce({
config: testGuideConfig,
config: {
...testGuideConfig,
steps: [
// remove step4 for this test to make step3 the last in the guide
testGuideConfig.steps[0],
testGuideConfig.steps[1],
testGuideConfig.steps[2],
],
},
});
apiService.setup(httpClient, true);
await apiService.completeGuideStep(testGuideId, testGuideLastStep);
// for this test step3 is the last step
const lastStepId = testGuideConfig.steps[2].id;
await apiService.completeGuideStep(testGuideId, lastStepId);
expect(httpClient.put).toHaveBeenCalledTimes(1);
// Verify the guide now has a "ready_to_complete" status and the last step is "complete"
@ -479,6 +505,7 @@ describe('GuidedOnboarding ApiService', () => {
testGuideStep2ActiveState.steps[0],
{ ...testGuideStep2ActiveState.steps[1], status: 'active' },
testGuideStep2ActiveState.steps[2],
testGuideStep2ActiveState.steps[3],
],
},
}),

View file

@ -23,6 +23,7 @@ import type {
GuideStep,
GuideStepIds,
GuideConfig,
GuideParams,
} from '@kbn/guided-onboarding';
import { API_BASE_PATH } from '../../common';
@ -360,7 +361,8 @@ export class ApiService implements GuidedOnboardingApi {
*/
public async completeGuideStep(
guideId: GuideId,
stepId: GuideStepIds
stepId: GuideStepIds,
params?: GuideParams
): Promise<{ pluginState: PluginState } | undefined> {
const pluginState = await firstValueFrom(this.fetchPluginState$());
// For now, returning undefined if consumer attempts to complete a step for a guide that isn't active
@ -395,6 +397,7 @@ export class ApiService implements GuidedOnboardingApi {
isActive: true,
status,
steps: updatedSteps,
params,
};
return await this.updatePluginState(

View file

@ -8,7 +8,13 @@
import { Observable } from 'rxjs';
import { HttpSetup } from '@kbn/core/public';
import type { GuideState, GuideId, GuideStepIds, GuideConfig } from '@kbn/guided-onboarding';
import type {
GuideState,
GuideId,
GuideStepIds,
GuideConfig,
GuideParams,
} from '@kbn/guided-onboarding';
import type { CloudStart } from '@kbn/cloud-plugin/public';
import type { PluginStatus, PluginState } from '../common';
@ -45,7 +51,8 @@ export interface GuidedOnboardingApi {
) => Promise<{ pluginState: PluginState } | undefined>;
completeGuideStep: (
guideId: GuideId,
stepId: GuideStepIds
stepId: GuideStepIds,
params?: GuideParams
) => Promise<{ pluginState: PluginState } | undefined>;
isGuidedOnboardingActiveForIntegration$: (integration?: string) => Observable<boolean>;
completeGuidedOnboardingForIntegration: (

View file

@ -53,6 +53,8 @@ export const registerPutPluginStateRoute = (router: IRouter) => {
id: schema.string(),
})
),
// params are dynamic values
params: schema.maybe(schema.object({}, { unknowns: 'allow' })),
})
),
}),

View file

@ -11,6 +11,7 @@ import {
testGuideStep1ActiveState,
testGuideNotActiveState,
mockPluginStateNotStarted,
testGuideParams,
} from '@kbn/guided-onboarding-plugin/public/services/api.mocks';
import {
guideStateSavedObjectsType,
@ -110,5 +111,19 @@ export default function testGetState({ getService }: FtrProviderContext) {
expect(response.body.pluginState).not.to.be.empty();
expect(response.body.pluginState.isActivePeriod).to.eql(true);
});
it('returns the dynamic params', async () => {
// Create an active guide
await createGuides(kibanaServer, [{ ...testGuideStep1ActiveState, params: testGuideParams }]);
// Create a plugin state
await createPluginState(kibanaServer, {
status: 'in_progress',
creationDate: new Date().toISOString(),
});
const response = await supertest.get(getStatePath).expect(200);
expect(response.body.pluginState.activeGuide.params).to.eql(testGuideParams);
});
});
}

View file

@ -10,6 +10,9 @@ import expect from '@kbn/expect';
import {
testGuideStep1ActiveState,
testGuideNotActiveState,
testGuideStep1InProgressState,
testGuideStep2ActiveState,
testGuideParams,
} from '@kbn/guided-onboarding-plugin/public/services/api.mocks';
import {
pluginStateSavedObjectsType,
@ -161,5 +164,29 @@ export default function testPutState({ getService }: FtrProviderContext) {
});
expect(kubernetesGuide.attributes.isActive).to.eql(true);
});
it('saves dynamic params if provided', async () => {
// create a guide
await createGuides(kibanaServer, [testGuideStep1InProgressState]);
// complete step1 with dynamic params
await supertest
.put(putStatePath)
.set('kbn-xsrf', 'true')
.send({
guide: {
...testGuideStep2ActiveState,
params: testGuideParams,
},
})
.expect(200);
// check that params object was saved
const testGuideSO = await kibanaServer.savedObjects.get({
type: guideStateSavedObjectsType,
id: testGuideId,
});
expect(testGuideSO.attributes.params).to.eql(testGuideParams);
});
});
}