diff --git a/examples/guided_onboarding_example/public/components/app.tsx b/examples/guided_onboarding_example/public/components/app.tsx index 41b9f62ddc075..1a1083a10f1f3 100755 --- a/examples/guided_onboarding_example/public/components/app.tsx +++ b/examples/guided_onboarding_example/public/components/app.tsx @@ -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 + p + ( + + )} + /> diff --git a/examples/guided_onboarding_example/public/components/main.tsx b/examples/guided_onboarding_example/public/components/main.tsx index 4e7c3843f5f34..d5f1febccfbce 100644 --- a/examples/guided_onboarding_example/public/components/main.tsx +++ b/examples/guided_onboarding_example/public/components/main.tsx @@ -345,6 +345,14 @@ export const Main = (props: MainProps) => { /> + + history.push('stepFour')}> + + + diff --git a/examples/guided_onboarding_example/public/components/step_four.tsx b/examples/guided_onboarding_example/public/components/step_four.tsx new file mode 100644 index 0000000000000..57df11d422519 --- /dev/null +++ b/examples/guided_onboarding_example/public/components/step_four.tsx @@ -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(false); + + useEffect(() => { + const subscription = guidedOnboardingApi + ?.isGuideStepActive$('testGuide', 'step4') + .subscribe((isStepActive) => { + setIsTourStepOpen(isStepActive); + }); + return () => subscription?.unsubscribe(); + }, [guidedOnboardingApi]); + + return ( + <> + + +

+ +

+
+
+ + +

+ {indexName: {indexName}} + ), + }} + /> +

+
+ + + { + await guidedOnboardingApi?.completeGuideStep('testGuide', 'step4'); + }} + > + Complete step 4 + +
+ + ); +}; diff --git a/examples/guided_onboarding_example/public/components/step_one.tsx b/examples/guided_onboarding_example/public/components/step_one.tsx index fd5cb132b6b91..632b902e14a3e 100644 --- a/examples/guided_onboarding_example/public/components/step_one.tsx +++ b/examples/guided_onboarding_example/public/components/step_one.tsx @@ -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(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." />

+

+ indexName, + }} + /> +

- -

Click this button to complete step 1.

- - } - isStepOpen={isTourStepOpen} - minWidth={300} - onFinish={() => setIsTourStepOpen(false)} - step={1} - stepsTotal={1} - title="Step 1" - anchorPosition="rightUp" - > - { - await guidedOnboardingApi?.completeGuideStep('testGuide', 'step1'); - }} - > - Complete step 1 - -
+ + + + } + > + setIndexName(e.target.value)} /> + + + + + +

Click this button to complete step 1.

+ + } + isStepOpen={isTourStepOpen} + minWidth={300} + onFinish={() => setIsTourStepOpen(false)} + step={1} + stepsTotal={1} + title="Step 1" + anchorPosition="rightUp" + > + { + await guidedOnboardingApi?.completeGuideStep('testGuide', 'step1', { + indexName, + }); + }} + > + Complete step 1 + +
+
+
+
); diff --git a/packages/kbn-guided-onboarding/index.ts b/packages/kbn-guided-onboarding/index.ts index 9ccaae3901b48..a72c60ac8d25e 100644 --- a/packages/kbn-guided-onboarding/index.ts +++ b/packages/kbn-guided-onboarding/index.ts @@ -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'; diff --git a/packages/kbn-guided-onboarding/src/common/test_guide_config.ts b/packages/kbn-guided-onboarding/src/common/test_guide_config.ts index a7944ef1d8bb8..875500fbb827c 100644 --- a/packages/kbn-guided-onboarding/src/common/test_guide_config.ts +++ b/packages/kbn-guided-onboarding/src/common/test_guide_config.ts @@ -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}', + }, + }, ], }; diff --git a/packages/kbn-guided-onboarding/src/types.ts b/packages/kbn-guided-onboarding/src/types.ts index 5f6da4b83ef54..bcf658ece6d3d 100644 --- a/packages/kbn-guided-onboarding/src/types.ts +++ b/packages/kbn-guided-onboarding/src/types.ts @@ -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; + 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; + /* + * 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; diff --git a/src/plugins/guided_onboarding/README.md b/src/plugins/guided_onboarding/README.md index 7dbf443f7f86e..1d1db6d13098a 100755 --- a/src/plugins/guided_onboarding/README.md +++ b/src/plugins/guided_onboarding/README.md @@ -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: diff --git a/src/plugins/guided_onboarding/public/components/get_step_location.test.ts b/src/plugins/guided_onboarding/public/components/get_step_location.test.ts new file mode 100644 index 0000000000000..98c72fb3ae0b2 --- /dev/null +++ b/src/plugins/guided_onboarding/public/components/get_step_location.test.ts @@ -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`); + }); +}); diff --git a/src/plugins/guided_onboarding/public/components/get_step_location.ts b/src/plugins/guided_onboarding/public/components/get_step_location.ts new file mode 100644 index 0000000000000..3e68b3af47eb5 --- /dev/null +++ b/src/plugins/guided_onboarding/public/components/get_step_location.ts @@ -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; +}; diff --git a/src/plugins/guided_onboarding/public/components/guide_panel.test.tsx b/src/plugins/guided_onboarding/public/components/guide_panel.test.tsx index 233d9c348c436..42440cd2587b2 100644 --- a/src/plugins/guided_onboarding/public/components/guide_panel.test.tsx +++ b/src/plugins/guided_onboarding/public/components/guide_panel.test.tsx @@ -457,7 +457,7 @@ describe('Guided setup', () => { expect( find('guidePanelStepDescription') - .last() + .first() .containsMatchingElement(

{testGuideConfig.steps[2].description}

) ).toBe(true); }); diff --git a/src/plugins/guided_onboarding/public/components/guide_panel.tsx b/src/plugins/guided_onboarding/public/components/guide_panel.tsx index 2d3ef51489081..a87b006eb55eb 100644 --- a/src/plugins/guided_onboarding/public/components/guide_panel.tsx +++ b/src/plugins/guided_onboarding/public/components/guide_panel.tsx @@ -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) { diff --git a/src/plugins/guided_onboarding/public/services/api.mocks.ts b/src/plugins/guided_onboarding/public/services/api.mocks.ts index 1d5dfc16a9145..3899eb61ff4bf 100644 --- a/src/plugins/guided_onboarding/public/services/api.mocks.ts +++ b/src/plugins/guided_onboarding/public/services/api.mocks.ts @@ -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', +}; diff --git a/src/plugins/guided_onboarding/public/services/api.test.ts b/src/plugins/guided_onboarding/public/services/api.service.test.ts similarity index 95% rename from src/plugins/guided_onboarding/public/services/api.test.ts rename to src/plugins/guided_onboarding/public/services/api.service.test.ts index 41c6a93faf27e..e6ce000bde594 100644 --- a/src/plugins/guided_onboarding/public/services/api.test.ts +++ b/src/plugins/guided_onboarding/public/services/api.service.test.ts @@ -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], ], }, }), diff --git a/src/plugins/guided_onboarding/public/services/api.service.ts b/src/plugins/guided_onboarding/public/services/api.service.ts index 949002b19ea6a..d3d20143f600d 100644 --- a/src/plugins/guided_onboarding/public/services/api.service.ts +++ b/src/plugins/guided_onboarding/public/services/api.service.ts @@ -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( diff --git a/src/plugins/guided_onboarding/public/types.ts b/src/plugins/guided_onboarding/public/types.ts index 1b0ccc7d925b3..1103c2ee350da 100755 --- a/src/plugins/guided_onboarding/public/types.ts +++ b/src/plugins/guided_onboarding/public/types.ts @@ -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; completeGuidedOnboardingForIntegration: ( diff --git a/src/plugins/guided_onboarding/server/routes/plugin_state_routes.ts b/src/plugins/guided_onboarding/server/routes/plugin_state_routes.ts index 997303c095098..dcd46984b166f 100644 --- a/src/plugins/guided_onboarding/server/routes/plugin_state_routes.ts +++ b/src/plugins/guided_onboarding/server/routes/plugin_state_routes.ts @@ -53,6 +53,8 @@ export const registerPutPluginStateRoute = (router: IRouter) => { id: schema.string(), }) ), + // params are dynamic values + params: schema.maybe(schema.object({}, { unknowns: 'allow' })), }) ), }), diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_menu.test.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_menu.test.tsx index 839983c9c6b32..d5c6377d20942 100644 --- a/src/plugins/unified_search/public/query_string_input/query_bar_menu.test.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_bar_menu.test.tsx @@ -230,7 +230,7 @@ describe('Querybar Menu component', () => { expect(languageSwitcher.length).toBeTruthy(); }); - it('should render the save query quick buttons', async () => { + it('should render the save query quick button', async () => { const newProps = { ...props, openQueryBarMenu: true, @@ -254,10 +254,6 @@ describe('Querybar Menu component', () => { '[data-test-subj="saved-query-management-save-changes-button"]' ); expect(saveChangesButton.length).toBeTruthy(); - const saveChangesAsNewButton = component.find( - '[data-test-subj="saved-query-management-save-as-new-button"]' - ); - expect(saveChangesAsNewButton.length).toBeTruthy(); }); it('should render all filter panel options by default', async () => { diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_menu.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_menu.tsx index a9342a65ea74f..9ba28c72c47e3 100644 --- a/src/plugins/unified_search/public/query_string_input/query_bar_menu.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_bar_menu.tsx @@ -32,7 +32,11 @@ import { export const strings = { getFilterSetButtonLabel: () => i18n.translate('unifiedSearch.filter.options.filterSetButtonLabel', { - defaultMessage: 'Saved query menu', + defaultMessage: 'Query menu', + }), + getSavedQueryPopoverSaveChangesButtonText: () => + i18n.translate('unifiedSearch.search.searchBar.savedQueryPopoverSaveChangesButtonText', { + defaultMessage: 'Update query', }), }; @@ -171,7 +175,10 @@ function QueryBarMenuComponent({ ); case 'saveForm': return ( - {saveFormComponent}]} /> + {saveFormComponent}]} + /> ); case 'saveAsNewForm': return ( diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_menu_panels.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_menu_panels.tsx index 20f155db53832..d7c8bd8b64a45 100644 --- a/src/plugins/unified_search/public/query_string_input/query_bar_menu_panels.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_bar_menu_panels.tsx @@ -63,7 +63,7 @@ export const strings = { }), getLoadOtherFilterSetLabel: () => i18n.translate('unifiedSearch.filter.options.loadOtherFilterSetLabel', { - defaultMessage: 'Load a different query', + defaultMessage: 'Load query', }), getLoadCurrentFilterSetLabel: () => i18n.translate('unifiedSearch.filter.options.loadCurrentFilterSetLabel', { @@ -71,7 +71,7 @@ export const strings = { }), getSaveAsNewFilterSetLabel: () => i18n.translate('unifiedSearch.filter.options.saveAsNewFilterSetLabel', { - defaultMessage: 'Save as new', + defaultMessage: 'Save query', }), getSaveFilterSetLabel: () => i18n.translate('unifiedSearch.filter.options.saveFilterSetLabel', { @@ -88,7 +88,7 @@ export const strings = { }), getSavedQueryPopoverSaveChangesButtonText: () => i18n.translate('unifiedSearch.search.searchBar.savedQueryPopoverSaveChangesButtonText', { - defaultMessage: 'Save changes', + defaultMessage: 'Update query', }), getSavedQueryPopoverSaveAsNewButtonAriaLabel: () => i18n.translate('unifiedSearch.search.searchBar.savedQueryPopoverSaveAsNewButtonAriaLabel', { @@ -100,7 +100,7 @@ export const strings = { }), getSaveCurrentFilterSetLabel: () => i18n.translate('unifiedSearch.filter.options.saveCurrentFilterSetLabel', { - defaultMessage: 'Save current query', + defaultMessage: 'Save as new', }), getApplyAllFiltersButtonLabel: () => i18n.translate('unifiedSearch.filter.options.applyAllFiltersButtonLabel', { @@ -238,10 +238,6 @@ export function QueryBarMenuPanels({ }; }; - const handleSaveAsNew = useCallback(() => { - setRenderedComponent('saveAsNewForm'); - }, [setRenderedComponent]); - const handleSave = useCallback(() => { setRenderedComponent('saveForm'); }, [setRenderedComponent]); @@ -413,38 +409,17 @@ export function QueryBarMenuPanels({ {savedQuery && savedQueryHasChanged && Boolean(showSaveQuery) && hasFiltersOrQuery && ( - - - - {strings.getSavedQueryPopoverSaveChangesButtonText()} - - - - - {strings.getSavedQueryPopoverSaveAsNewButtonText()} - - - + {strings.getSavedQueryPopoverSaveChangesButtonText()} + )} diff --git a/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.test.tsx b/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.test.tsx index 01837e26bb892..c891174986510 100644 --- a/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.test.tsx +++ b/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.test.tsx @@ -136,7 +136,7 @@ describe('Saved query management list component', () => { .find('[data-test-subj="saved-query-management-apply-changes-button"]') .first() .text() - ).toBe('Apply query'); + ).toBe('Load query'); const newProps = { ...props, diff --git a/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.tsx b/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.tsx index daaf523abf68b..5ebb89bd88afc 100644 --- a/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.tsx +++ b/src/plugins/unified_search/public/saved_query_management/saved_query_management_list.tsx @@ -309,22 +309,7 @@ export function SavedQueryManagementList({ )} - - {canEditSavedObjects && ( - - - Manage - - - )} + - {hasFiltersOrQuery - ? i18n.translate( - 'unifiedSearch.search.searchBar.savedQueryPopoverReplaceFilterSetLabel', - { - defaultMessage: 'Load query', - } - ) - : i18n.translate( - 'unifiedSearch.search.searchBar.savedQueryPopoverApplyFilterSetLabel', - { - defaultMessage: 'Apply query', - } - )} + {i18n.translate( + 'unifiedSearch.search.searchBar.savedQueryPopoverApplyFilterSetLabel', + { + defaultMessage: 'Load query', + } + )} + {canEditSavedObjects && ( + + + {i18n.translate('unifiedSearch.search.searchBar.savedQueryPopoverManageLabel', { + defaultMessage: 'Manage saved objects', + })} + + + )} {showDeletionConfirmationModal && toBeDeletedSavedQuery && ( diff --git a/test/api_integration/apis/guided_onboarding/get_state.ts b/test/api_integration/apis/guided_onboarding/get_state.ts index 489bf26830585..2bb428c6b353a 100644 --- a/test/api_integration/apis/guided_onboarding/get_state.ts +++ b/test/api_integration/apis/guided_onboarding/get_state.ts @@ -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); + }); }); } diff --git a/test/api_integration/apis/guided_onboarding/put_state.ts b/test/api_integration/apis/guided_onboarding/put_state.ts index 7b287b56bb77c..d22366dc080e0 100644 --- a/test/api_integration/apis/guided_onboarding/put_state.ts +++ b/test/api_integration/apis/guided_onboarding/put_state.ts @@ -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); + }); }); } diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index aa07a3ea5fe94..cb0139fb29c47 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -5801,7 +5801,6 @@ "unifiedSearch.search.searchBar.savedQueryPopoverApplyFilterSetLabel": "Appliquer la requête enregistrée", "unifiedSearch.search.searchBar.savedQueryPopoverConfirmDeletionCancelButtonText": "Annuler", "unifiedSearch.search.searchBar.savedQueryPopoverConfirmDeletionConfirmButtonText": "Supprimer", - "unifiedSearch.search.searchBar.savedQueryPopoverReplaceFilterSetLabel": "Remplacer par la requête enregistrée sélectionnée", "unifiedSearch.search.searchBar.savedQueryPopoverSaveAsNewButtonAriaLabel": "Enregistrer en tant que nouvelle requête enregistrée", "unifiedSearch.search.searchBar.savedQueryPopoverSaveAsNewButtonText": "Enregistrer en tant que nouvelle", "unifiedSearch.search.searchBar.savedQueryPopoverSaveChangesButtonText": "Enregistrer les modifications", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 5896cf945658d..65d5fadf90c9d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5802,7 +5802,6 @@ "unifiedSearch.search.searchBar.savedQueryPopoverApplyFilterSetLabel": "保存されたクエリの適用", "unifiedSearch.search.searchBar.savedQueryPopoverConfirmDeletionCancelButtonText": "キャンセル", "unifiedSearch.search.searchBar.savedQueryPopoverConfirmDeletionConfirmButtonText": "削除", - "unifiedSearch.search.searchBar.savedQueryPopoverReplaceFilterSetLabel": "選択した保存されたクエリで置換", "unifiedSearch.search.searchBar.savedQueryPopoverSaveAsNewButtonAriaLabel": "新規保存クエリを保存", "unifiedSearch.search.searchBar.savedQueryPopoverSaveAsNewButtonText": "新規保存", "unifiedSearch.search.searchBar.savedQueryPopoverSaveChangesButtonText": "変更を保存", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 8cdf63b37c4ed..f5a0d3b49535f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5802,7 +5802,6 @@ "unifiedSearch.search.searchBar.savedQueryPopoverApplyFilterSetLabel": "应用已保存查询", "unifiedSearch.search.searchBar.savedQueryPopoverConfirmDeletionCancelButtonText": "取消", "unifiedSearch.search.searchBar.savedQueryPopoverConfirmDeletionConfirmButtonText": "删除", - "unifiedSearch.search.searchBar.savedQueryPopoverReplaceFilterSetLabel": "替换为选定已保存查询", "unifiedSearch.search.searchBar.savedQueryPopoverSaveAsNewButtonAriaLabel": "另存为新的已保存查询", "unifiedSearch.search.searchBar.savedQueryPopoverSaveAsNewButtonText": "另存为新的", "unifiedSearch.search.searchBar.savedQueryPopoverSaveChangesButtonText": "保存更改",