mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[Guided onboarding] Update header button logic (#144634)
## Summary Fixes https://github.com/elastic/kibana/issues/141129 Fixes https://github.com/elastic/kibana/issues/144515 This PR introduces a new state to the guided onboarding plugin. The state keeps track of the `creationDate` and of the overall `status` of the plugin. The creation date allows us to detect an "active" period during which the header button will be displayed more prominently in the header. Currently, the active period is set to 30 days. During this time, if the user has not started any guide, has quit a guide before completion or skipped the guide on the landing page, the header button will be displayed and when clicked, redirect the user to the landing page to start/continue a guide. Also this PR adds a check for Cloud deployments and prevents the code from sending any API requests when not on Cloud, because guided onboarding is disabled on prem. #### Screenshot <img width="298" alt="Screenshot 2022-11-10 at 18 42 18" src="https://user-images.githubusercontent.com/6585477/201168414-391a7cd4-0709-492b-9001-1432b5bed3c8.png"> ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [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: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
e2d3bb9dec
commit
879b101669
37 changed files with 1586 additions and 979 deletions
|
@ -113,7 +113,10 @@ export const Main = (props: MainProps) => {
|
|||
guideId: selectedGuide!,
|
||||
};
|
||||
|
||||
const response = await guidedOnboardingApi?.updateGuideState(updatedGuideState, true);
|
||||
const response = await guidedOnboardingApi?.updatePluginState(
|
||||
{ status: 'in_progress', guide: updatedGuideState },
|
||||
true
|
||||
);
|
||||
if (response) {
|
||||
notifications.toasts.addSuccess(
|
||||
i18n.translate('guidedOnboardingExample.updateGuideState.toastLabel', {
|
||||
|
|
|
@ -61,7 +61,7 @@ pageLoadAssetSize:
|
|||
globalSearchProviders: 25554
|
||||
graph: 31504
|
||||
grokdebugger: 26779
|
||||
guidedOnboarding: 26875
|
||||
guidedOnboarding: 42965
|
||||
home: 30182
|
||||
indexLifecycleManagement: 107090
|
||||
indexManagement: 140608
|
||||
|
|
|
@ -92,6 +92,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
|
|||
"fleet-preconfiguration-deletion-record": "7b28f200513c28ae774f1b7d7d7906954e3c6e16",
|
||||
"graph-workspace": "3342f2cd561afdde8f42f5fb284bf550dee8ebb5",
|
||||
"guided-onboarding-guide-state": "561db8d481b131a2bbf46b1e534d6ce960255135",
|
||||
"guided-onboarding-plugin-state": "a802ed58e9d0076b9632c59d7943861ba476f99c",
|
||||
"index-pattern": "48e77ca393c254e93256f11a7cdc0232dd754c08",
|
||||
"infrastructure-monitoring-log-view": "e2c78c1076bd35e57d7c5fa1b410e5c126d12327",
|
||||
"infrastructure-ui-source": "7c8dbbc0a608911f1b683a944f4a65383f6153ed",
|
||||
|
|
|
@ -62,6 +62,7 @@ const previouslyRegisteredTypes = [
|
|||
'graph-workspace',
|
||||
'guided-setup-state',
|
||||
'guided-onboarding-guide-state',
|
||||
'guided-onboarding-plugin-state',
|
||||
'index-pattern',
|
||||
'infrastructure-monitoring-log-view',
|
||||
'infrastructure-ui-source',
|
||||
|
|
26
src/plugins/guided_onboarding/common/types.ts
Normal file
26
src/plugins/guided_onboarding/common/types.ts
Normal 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 { GuideState } from '@kbn/guided-onboarding';
|
||||
|
||||
/**
|
||||
* Guided onboarding overall status:
|
||||
* not_started: no guides have been started yet
|
||||
* in_progress: a guide is currently active
|
||||
* complete: at least one guide has been completed
|
||||
* quit: the user quit a guide before completion
|
||||
* skipped: the user skipped on the landing page
|
||||
*/
|
||||
export type PluginStatus = 'not_started' | 'in_progress' | 'complete' | 'quit' | 'skipped';
|
||||
|
||||
export interface PluginState {
|
||||
status: PluginStatus;
|
||||
// a specific period after deployment creation when guided onboarding UI is highlighted
|
||||
isActivePeriod: boolean;
|
||||
activeGuide?: GuideState;
|
||||
}
|
|
@ -11,13 +11,15 @@ import { EuiButton } from '@elastic/eui';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import type { GuideState } from '@kbn/guided-onboarding';
|
||||
|
||||
import type { PluginState } from '../../common/types';
|
||||
import { getStepConfig } from '../services/helpers';
|
||||
import { GuideButtonPopover } from './guide_button_popover';
|
||||
|
||||
interface GuideButtonProps {
|
||||
guideState: GuideState;
|
||||
pluginState: PluginState | undefined;
|
||||
toggleGuidePanel: () => void;
|
||||
isGuidePanelOpen: boolean;
|
||||
navigateToLandingPage: () => void;
|
||||
}
|
||||
|
||||
const getStepNumber = (state: GuideState): number | undefined => {
|
||||
|
@ -39,12 +41,45 @@ const getStepNumber = (state: GuideState): number | undefined => {
|
|||
};
|
||||
|
||||
export const GuideButton = ({
|
||||
guideState,
|
||||
pluginState,
|
||||
toggleGuidePanel,
|
||||
isGuidePanelOpen,
|
||||
navigateToLandingPage,
|
||||
}: GuideButtonProps) => {
|
||||
const stepNumber = getStepNumber(guideState);
|
||||
const stepReadyToComplete = guideState.steps.find((step) => step.status === 'ready_to_complete');
|
||||
// TODO handle loading, error state
|
||||
// https://github.com/elastic/kibana/issues/139799, https://github.com/elastic/kibana/issues/139798
|
||||
|
||||
// if there is no active guide
|
||||
if (!pluginState || !pluginState.activeGuide || !pluginState.activeGuide.isActive) {
|
||||
// if still active period and the user has not started a guide or skipped the guide,
|
||||
// display the button that redirects to the landing page
|
||||
if (
|
||||
!(
|
||||
pluginState?.isActivePeriod &&
|
||||
(pluginState?.status === 'not_started' || pluginState?.status === 'skipped')
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
} else {
|
||||
return (
|
||||
<EuiButton
|
||||
onClick={navigateToLandingPage}
|
||||
color="success"
|
||||
fill
|
||||
size="s"
|
||||
data-test-subj="guideButtonRedirect"
|
||||
>
|
||||
{i18n.translate('guidedOnboarding.guidedSetupRedirectButtonLabel', {
|
||||
defaultMessage: 'Setup guide',
|
||||
})}
|
||||
</EuiButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
const stepNumber = getStepNumber(pluginState.activeGuide);
|
||||
const stepReadyToComplete = pluginState.activeGuide.steps.find(
|
||||
(step) => step.status === 'ready_to_complete'
|
||||
);
|
||||
const button = (
|
||||
<EuiButton
|
||||
onClick={toggleGuidePanel}
|
||||
|
@ -66,7 +101,7 @@ export const GuideButton = ({
|
|||
</EuiButton>
|
||||
);
|
||||
if (stepReadyToComplete) {
|
||||
const stepConfig = getStepConfig(guideState.guideId, stepReadyToComplete.id);
|
||||
const stepConfig = getStepConfig(pluginState.activeGuide.guideId, stepReadyToComplete.id);
|
||||
// check if the stepConfig has manualCompletion info
|
||||
if (stepConfig && stepConfig.manualCompletion) {
|
||||
return (
|
||||
|
|
|
@ -11,77 +11,46 @@ import React from 'react';
|
|||
|
||||
import { applicationServiceMock } from '@kbn/core-application-browser-mocks';
|
||||
import { httpServiceMock } from '@kbn/core/public/mocks';
|
||||
import { HttpSetup } from '@kbn/core/public';
|
||||
import type { GuideState } from '@kbn/guided-onboarding';
|
||||
import type { HttpSetup } from '@kbn/core/public';
|
||||
import { registerTestBed, TestBed } from '@kbn/test-jest-helpers';
|
||||
|
||||
import type { PluginState } from '../../common/types';
|
||||
import { guidesConfig } from '../constants/guides_config';
|
||||
import { apiService } from '../services/api';
|
||||
import type { GuidedOnboardingApi } from '../types';
|
||||
import {
|
||||
testGuideStep1ActiveState,
|
||||
testGuideStep1InProgressState,
|
||||
testGuideStep2InProgressState,
|
||||
testGuideStep2ReadyToCompleteState,
|
||||
testGuideStep3ActiveState,
|
||||
readyToCompleteGuideState,
|
||||
mockPluginStateNotStarted,
|
||||
} from '../services/api.mocks';
|
||||
import { GuidePanel } from './guide_panel';
|
||||
import { registerTestBed, TestBed } from '@kbn/test-jest-helpers';
|
||||
|
||||
const applicationMock = applicationServiceMock.createStartContract();
|
||||
|
||||
const mockActiveTestGuideState: GuideState = {
|
||||
guideId: 'testGuide',
|
||||
isActive: true,
|
||||
status: 'in_progress',
|
||||
steps: [
|
||||
{
|
||||
id: 'step1',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
status: 'inactive',
|
||||
},
|
||||
{
|
||||
id: 'step3',
|
||||
status: 'inactive',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const mockInProgressTestGuideState: GuideState = {
|
||||
...mockActiveTestGuideState,
|
||||
steps: [
|
||||
{
|
||||
...mockActiveTestGuideState.steps[0],
|
||||
status: 'in_progress',
|
||||
},
|
||||
mockActiveTestGuideState.steps[1],
|
||||
mockActiveTestGuideState.steps[2],
|
||||
],
|
||||
};
|
||||
|
||||
const mockReadyToCompleteTestGuideState: GuideState = {
|
||||
...mockActiveTestGuideState,
|
||||
steps: [
|
||||
{
|
||||
...mockActiveTestGuideState.steps[0],
|
||||
status: 'complete',
|
||||
},
|
||||
{
|
||||
...mockActiveTestGuideState.steps[1],
|
||||
status: 'ready_to_complete',
|
||||
},
|
||||
mockActiveTestGuideState.steps[2],
|
||||
],
|
||||
};
|
||||
|
||||
const updateComponentWithState = async (
|
||||
component: TestBed['component'],
|
||||
guideState: GuideState,
|
||||
isPanelOpen: boolean
|
||||
const setupComponentWithPluginStateMock = async (
|
||||
httpClient: jest.Mocked<HttpSetup>,
|
||||
pluginState: PluginState
|
||||
) => {
|
||||
httpClient.get.mockResolvedValue({
|
||||
pluginState,
|
||||
});
|
||||
apiService.setup(httpClient, true);
|
||||
return await setupGuidePanelComponent(apiService);
|
||||
};
|
||||
|
||||
const setupGuidePanelComponent = async (api: GuidedOnboardingApi) => {
|
||||
let testBed: TestBed;
|
||||
const GuidePanelComponent = () => <GuidePanel application={applicationMock} api={api} />;
|
||||
await act(async () => {
|
||||
await apiService.updateGuideState(guideState, isPanelOpen);
|
||||
testBed = registerTestBed(GuidePanelComponent)();
|
||||
});
|
||||
|
||||
component.update();
|
||||
};
|
||||
|
||||
const getGuidePanel = () => () => {
|
||||
return <GuidePanel application={applicationMock} api={apiService} />;
|
||||
testBed!.component.update();
|
||||
return testBed!;
|
||||
};
|
||||
|
||||
describe('Guided setup', () => {
|
||||
|
@ -90,18 +59,8 @@ describe('Guided setup', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
httpClient = httpServiceMock.createStartContract({ basePath: '/base/path' });
|
||||
// Set default state on initial request (no active guides)
|
||||
httpClient.get.mockResolvedValue({
|
||||
state: [],
|
||||
});
|
||||
apiService.setup(httpClient);
|
||||
|
||||
await act(async () => {
|
||||
const GuidePanelComponent = getGuidePanel();
|
||||
testBed = registerTestBed(GuidePanelComponent)();
|
||||
});
|
||||
|
||||
testBed.component.update();
|
||||
// Default state is not started
|
||||
testBed = await setupComponentWithPluginStateMock(httpClient, mockPluginStateNotStarted);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -109,124 +68,190 @@ describe('Guided setup', () => {
|
|||
});
|
||||
|
||||
describe('Button component', () => {
|
||||
test('should be hidden in there is no guide state', async () => {
|
||||
const { exists } = testBed;
|
||||
expect(exists('guideButton')).toBe(false);
|
||||
expect(exists('guidePanel')).toBe(false);
|
||||
describe('when a guide is active', () => {
|
||||
it('button is enabled', async () => {
|
||||
const { exists, find } = await setupComponentWithPluginStateMock(httpClient, {
|
||||
status: 'in_progress',
|
||||
isActivePeriod: true,
|
||||
activeGuide: testGuideStep1ActiveState,
|
||||
});
|
||||
expect(exists('guideButton')).toBe(true);
|
||||
expect(find('guideButton').text()).toEqual('Setup guide');
|
||||
expect(exists('guideButtonRedirect')).toBe(false);
|
||||
});
|
||||
|
||||
test('button shows the step number in the button label if a step is active', async () => {
|
||||
const { exists, find } = await setupComponentWithPluginStateMock(httpClient, {
|
||||
status: 'in_progress',
|
||||
isActivePeriod: true,
|
||||
activeGuide: testGuideStep1InProgressState,
|
||||
});
|
||||
|
||||
expect(exists('guideButton')).toBe(true);
|
||||
expect(find('guideButton').text()).toEqual('Setup guide: step 1');
|
||||
expect(exists('guideButtonRedirect')).toBe(false);
|
||||
});
|
||||
|
||||
test('shows the step number in the button label if a step is ready to complete', async () => {
|
||||
const { exists, find } = await setupComponentWithPluginStateMock(httpClient, {
|
||||
status: 'in_progress',
|
||||
isActivePeriod: true,
|
||||
activeGuide: testGuideStep2ReadyToCompleteState,
|
||||
});
|
||||
|
||||
expect(exists('guideButton')).toBe(true);
|
||||
expect(find('guideButton').text()).toEqual('Setup guide: step 2');
|
||||
expect(exists('guideButtonRedirect')).toBe(false);
|
||||
});
|
||||
|
||||
test('shows the manual completion popover if a step is ready to complete', async () => {
|
||||
const { exists } = await setupComponentWithPluginStateMock(httpClient, {
|
||||
status: 'in_progress',
|
||||
isActivePeriod: true,
|
||||
activeGuide: testGuideStep2ReadyToCompleteState,
|
||||
});
|
||||
|
||||
expect(exists('manualCompletionPopover')).toBe(true);
|
||||
});
|
||||
|
||||
test('shows no manual completion popover if a step is in progress', async () => {
|
||||
const { exists } = await setupComponentWithPluginStateMock(httpClient, {
|
||||
status: 'in_progress',
|
||||
isActivePeriod: true,
|
||||
activeGuide: testGuideStep1InProgressState,
|
||||
});
|
||||
|
||||
expect(exists('manualCompletionPopoverPanel')).toBe(false);
|
||||
});
|
||||
|
||||
it('shows the button if after the active period', async () => {
|
||||
const { exists, find } = await setupComponentWithPluginStateMock(httpClient, {
|
||||
status: 'in_progress',
|
||||
isActivePeriod: false,
|
||||
activeGuide: testGuideStep1ActiveState,
|
||||
});
|
||||
expect(exists('guideButton')).toBe(true);
|
||||
expect(find('guideButton').text()).toEqual('Setup guide');
|
||||
expect(exists('guideButtonRedirect')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('should be hidden if the guide is not active', async () => {
|
||||
const { component, exists } = testBed;
|
||||
describe('when no guide is active', () => {
|
||||
describe('when in active period', () => {
|
||||
// mock state is by default { status: 'not_started', isActivePeriod: true }
|
||||
test('shows redirect button when no guide has been started yet', () => {
|
||||
const { exists } = testBed;
|
||||
expect(exists('guideButtonRedirect')).toBe(true);
|
||||
expect(exists('guideButton')).toBe(false);
|
||||
});
|
||||
|
||||
await updateComponentWithState(
|
||||
component,
|
||||
{ ...mockActiveTestGuideState, isActive: false },
|
||||
true
|
||||
);
|
||||
test('shows redirect button when a user skipped on the landing page', async () => {
|
||||
const { exists } = await setupComponentWithPluginStateMock(httpClient, {
|
||||
status: 'skipped',
|
||||
isActivePeriod: true,
|
||||
});
|
||||
|
||||
expect(exists('guideButton')).toBe(false);
|
||||
expect(exists('guidePanel')).toBe(false);
|
||||
});
|
||||
expect(exists('guideButtonRedirect')).toBe(true);
|
||||
expect(exists('guideButton')).toBe(false);
|
||||
});
|
||||
|
||||
test('should be enabled if there is an active guide', async () => {
|
||||
const { exists, component, find } = testBed;
|
||||
test('hides redirect button when a user quit the guide', async () => {
|
||||
const { exists } = await setupComponentWithPluginStateMock(httpClient, {
|
||||
status: 'quit',
|
||||
isActivePeriod: true,
|
||||
});
|
||||
|
||||
// Enable the "test" guide
|
||||
await updateComponentWithState(component, mockActiveTestGuideState, true);
|
||||
expect(exists('guideButtonRedirect')).toBe(false);
|
||||
expect(exists('guideButton')).toBe(false);
|
||||
});
|
||||
|
||||
expect(exists('guideButton')).toBe(true);
|
||||
expect(find('guideButton').text()).toEqual('Setup guide');
|
||||
});
|
||||
test('hides the button if the user completed a guide', async () => {
|
||||
const { exists } = await setupComponentWithPluginStateMock(httpClient, {
|
||||
status: 'complete',
|
||||
isActivePeriod: true,
|
||||
});
|
||||
|
||||
test('should show the step number in the button label if a step is active', async () => {
|
||||
const { component, find } = testBed;
|
||||
expect(exists('guideButtonRedirect')).toBe(false);
|
||||
expect(exists('guideButton')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
await updateComponentWithState(component, mockInProgressTestGuideState, true);
|
||||
describe('when not in active period', () => {
|
||||
test('hides the button if no guide has been started yet', async () => {
|
||||
const { exists } = await setupComponentWithPluginStateMock(httpClient, {
|
||||
status: 'not_started',
|
||||
isActivePeriod: false,
|
||||
});
|
||||
expect(exists('guideButtonRedirect')).toBe(false);
|
||||
expect(exists('guideButton')).toBe(false);
|
||||
});
|
||||
|
||||
expect(find('guideButton').text()).toEqual('Setup guide: step 1');
|
||||
});
|
||||
test('hides the button if a user quit the guide', async () => {
|
||||
const { exists } = await setupComponentWithPluginStateMock(httpClient, {
|
||||
status: 'quit',
|
||||
isActivePeriod: false,
|
||||
});
|
||||
expect(exists('guideButtonRedirect')).toBe(false);
|
||||
expect(exists('guideButton')).toBe(false);
|
||||
});
|
||||
|
||||
test('shows the step number in the button label if a step is ready to complete', async () => {
|
||||
const { component, find } = testBed;
|
||||
test('hides the button when a user skipped on the landing page', async () => {
|
||||
const { exists } = await setupComponentWithPluginStateMock(httpClient, {
|
||||
status: 'skipped',
|
||||
isActivePeriod: false,
|
||||
});
|
||||
expect(exists('guideButtonRedirect')).toBe(false);
|
||||
expect(exists('guideButton')).toBe(false);
|
||||
});
|
||||
|
||||
await updateComponentWithState(component, mockReadyToCompleteTestGuideState, true);
|
||||
|
||||
expect(find('guideButton').text()).toEqual('Setup guide: step 2');
|
||||
});
|
||||
|
||||
test('shows the manual completion popover if a step is ready to complete', async () => {
|
||||
const { component, exists } = testBed;
|
||||
|
||||
await updateComponentWithState(component, mockReadyToCompleteTestGuideState, false);
|
||||
|
||||
expect(exists('manualCompletionPopover')).toBe(true);
|
||||
});
|
||||
|
||||
test('shows no manual completion popover if a step is in progress', async () => {
|
||||
const { component, exists } = testBed;
|
||||
|
||||
await updateComponentWithState(component, mockInProgressTestGuideState, false);
|
||||
|
||||
expect(exists('manualCompletionPopoverPanel')).toBe(false);
|
||||
test('hides the button if the user completed a guide', async () => {
|
||||
const { exists } = await setupComponentWithPluginStateMock(httpClient, {
|
||||
status: 'complete',
|
||||
isActivePeriod: false,
|
||||
});
|
||||
expect(exists('guideButtonRedirect')).toBe(false);
|
||||
expect(exists('guideButton')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Panel component', () => {
|
||||
test('should be enabled if a guide is activated', async () => {
|
||||
const { exists, component, find } = testBed;
|
||||
|
||||
await updateComponentWithState(component, mockActiveTestGuideState, true);
|
||||
test('if a guide is active, the button click opens the panel', async () => {
|
||||
const { exists, find, component } = await setupComponentWithPluginStateMock(httpClient, {
|
||||
status: 'in_progress',
|
||||
isActivePeriod: true,
|
||||
activeGuide: testGuideStep1ActiveState,
|
||||
});
|
||||
find('guideButton').simulate('click');
|
||||
component.update();
|
||||
|
||||
expect(exists('guidePanel')).toBe(true);
|
||||
expect(exists('guideProgress')).toBe(false);
|
||||
expect(find('guidePanelStep').length).toEqual(guidesConfig.testGuide.steps.length);
|
||||
});
|
||||
|
||||
test('should show the progress bar if the first step has been completed', async () => {
|
||||
const { component, exists } = testBed;
|
||||
|
||||
const mockCompleteTestGuideState: GuideState = {
|
||||
...mockActiveTestGuideState,
|
||||
steps: [
|
||||
{
|
||||
...mockActiveTestGuideState.steps[0],
|
||||
status: 'complete',
|
||||
},
|
||||
mockActiveTestGuideState.steps[1],
|
||||
mockActiveTestGuideState.steps[2],
|
||||
],
|
||||
};
|
||||
|
||||
await updateComponentWithState(component, mockCompleteTestGuideState, true);
|
||||
test('shows the progress bar if the first step has been completed', async () => {
|
||||
const { exists, find, component } = await setupComponentWithPluginStateMock(httpClient, {
|
||||
status: 'in_progress',
|
||||
isActivePeriod: true,
|
||||
activeGuide: testGuideStep2InProgressState,
|
||||
});
|
||||
find('guideButton').simulate('click');
|
||||
component.update();
|
||||
|
||||
expect(exists('guidePanel')).toBe(true);
|
||||
expect(exists('guideProgress')).toBe(true);
|
||||
});
|
||||
|
||||
test('should show the completed state when all steps has been completed', async () => {
|
||||
const { component, exists, find } = testBed;
|
||||
|
||||
const readyToCompleteGuideState: GuideState = {
|
||||
guideId: 'testGuide',
|
||||
status: 'ready_to_complete',
|
||||
isActive: true,
|
||||
steps: [
|
||||
{
|
||||
id: 'step1',
|
||||
status: 'complete',
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
status: 'complete',
|
||||
},
|
||||
{
|
||||
id: 'step3',
|
||||
status: 'complete',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await updateComponentWithState(component, readyToCompleteGuideState, true);
|
||||
test('shows the completed state when all steps has been completed', async () => {
|
||||
const { exists, find, component } = await setupComponentWithPluginStateMock(httpClient, {
|
||||
status: 'in_progress',
|
||||
isActivePeriod: true,
|
||||
activeGuide: { ...readyToCompleteGuideState, status: 'ready_to_complete' },
|
||||
});
|
||||
find('guideButton').simulate('click');
|
||||
component.update();
|
||||
|
||||
expect(find('guideTitle').text()).toContain('Well done');
|
||||
expect(find('guideDescription').text()).toContain(
|
||||
|
@ -235,28 +260,30 @@ describe('Guided setup', () => {
|
|||
expect(exists('onboarding--completeGuideButton--testGuide')).toBe(true);
|
||||
});
|
||||
|
||||
test('should not show the completed state when the last step is not marked as complete', async () => {
|
||||
const { component, exists, find } = testBed;
|
||||
|
||||
const mockCompleteTestGuideState: GuideState = {
|
||||
...mockActiveTestGuideState,
|
||||
steps: [
|
||||
{
|
||||
id: mockActiveTestGuideState.steps[0].id,
|
||||
status: 'complete',
|
||||
},
|
||||
{
|
||||
id: mockActiveTestGuideState.steps[1].id,
|
||||
status: 'complete',
|
||||
},
|
||||
{
|
||||
id: mockActiveTestGuideState.steps[2].id,
|
||||
status: 'complete',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await updateComponentWithState(component, mockCompleteTestGuideState, true);
|
||||
test(`doesn't show the completed state when the last step is not marked as complete`, async () => {
|
||||
const { exists, find, component } = await setupComponentWithPluginStateMock(httpClient, {
|
||||
status: 'in_progress',
|
||||
isActivePeriod: true,
|
||||
activeGuide: {
|
||||
...testGuideStep1ActiveState,
|
||||
steps: [
|
||||
{
|
||||
...testGuideStep1ActiveState.steps[0],
|
||||
status: 'complete',
|
||||
},
|
||||
{
|
||||
...testGuideStep1ActiveState.steps[1],
|
||||
status: 'complete',
|
||||
},
|
||||
{
|
||||
...testGuideStep1ActiveState.steps[2],
|
||||
status: 'ready_to_complete',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
find('guideButton').simulate('click');
|
||||
component.update();
|
||||
|
||||
expect(find('guideTitle').text()).not.toContain('Well done');
|
||||
expect(find('guideDescription').text()).not.toContain(
|
||||
|
@ -283,9 +310,21 @@ describe('Guided setup', () => {
|
|||
};
|
||||
|
||||
test('can start a step if step has not been started', async () => {
|
||||
const { component, find, exists } = testBed;
|
||||
|
||||
await updateComponentWithState(component, mockActiveTestGuideState, true);
|
||||
httpClient.put.mockResolvedValueOnce({
|
||||
pluginState: {
|
||||
status: 'in_progress',
|
||||
isActivePeriod: true,
|
||||
activeGuide: testGuideStep1InProgressState,
|
||||
},
|
||||
});
|
||||
testBed = await setupComponentWithPluginStateMock(httpClient, {
|
||||
status: 'in_progress',
|
||||
isActivePeriod: true,
|
||||
activeGuide: testGuideStep1ActiveState,
|
||||
});
|
||||
const { exists, find, component } = testBed;
|
||||
find('guideButton').simulate('click');
|
||||
component.update();
|
||||
|
||||
expect(find('onboarding--stepButton--testGuide--step1').text()).toEqual('Start');
|
||||
|
||||
|
@ -295,9 +334,21 @@ describe('Guided setup', () => {
|
|||
});
|
||||
|
||||
test('can continue a step if step is in progress', async () => {
|
||||
const { component, find, exists } = testBed;
|
||||
|
||||
await updateComponentWithState(component, mockInProgressTestGuideState, true);
|
||||
httpClient.put.mockResolvedValueOnce({
|
||||
pluginState: {
|
||||
status: 'in_progress',
|
||||
isActivePeriod: true,
|
||||
activeGuide: testGuideStep1InProgressState,
|
||||
},
|
||||
});
|
||||
testBed = await setupComponentWithPluginStateMock(httpClient, {
|
||||
status: 'in_progress',
|
||||
isActivePeriod: true,
|
||||
activeGuide: testGuideStep1InProgressState,
|
||||
});
|
||||
const { exists, find, component } = testBed;
|
||||
find('guideButton').simulate('click');
|
||||
component.update();
|
||||
|
||||
expect(find('onboarding--stepButton--testGuide--step1').text()).toEqual('Continue');
|
||||
|
||||
|
@ -307,9 +358,21 @@ describe('Guided setup', () => {
|
|||
});
|
||||
|
||||
test('can mark a step "done" if step is ready to complete', async () => {
|
||||
const { component, find, exists } = testBed;
|
||||
|
||||
await updateComponentWithState(component, mockReadyToCompleteTestGuideState, true);
|
||||
httpClient.put.mockResolvedValueOnce({
|
||||
pluginState: {
|
||||
status: 'in_progress',
|
||||
isActivePeriod: true,
|
||||
activeGuide: testGuideStep3ActiveState,
|
||||
},
|
||||
});
|
||||
testBed = await setupComponentWithPluginStateMock(httpClient, {
|
||||
status: 'in_progress',
|
||||
isActivePeriod: true,
|
||||
activeGuide: testGuideStep2ReadyToCompleteState,
|
||||
});
|
||||
const { exists, find, component } = testBed;
|
||||
find('guideButton').simulate('click');
|
||||
component.update();
|
||||
|
||||
expect(find('onboarding--stepButton--testGuide--step2').text()).toEqual('Mark done');
|
||||
|
||||
|
@ -317,38 +380,18 @@ describe('Guided setup', () => {
|
|||
|
||||
// The guide panel should remain open after marking a step done
|
||||
expect(exists('guidePanel')).toBe(true);
|
||||
// Dependent on the Search guide config, which expects step 3 to start
|
||||
// Dependent on the Test guide config, which expects step 3 to start
|
||||
expect(find('onboarding--stepButton--testGuide--step3').text()).toEqual('Start');
|
||||
});
|
||||
|
||||
test('should render the step description as a paragraph if it is only one sentence', async () => {
|
||||
const { component, find } = testBed;
|
||||
|
||||
const mockSingleSentenceStepDescriptionGuideState: GuideState = {
|
||||
guideId: 'testGuide',
|
||||
isActive: true,
|
||||
test('renders the step description as a paragraph', async () => {
|
||||
const { find, component } = await setupComponentWithPluginStateMock(httpClient, {
|
||||
status: 'in_progress',
|
||||
steps: [
|
||||
{
|
||||
id: 'step1',
|
||||
status: 'complete',
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
status: 'complete',
|
||||
},
|
||||
{
|
||||
id: 'step3',
|
||||
status: 'in_progress',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await updateComponentWithState(
|
||||
component,
|
||||
mockSingleSentenceStepDescriptionGuideState,
|
||||
true
|
||||
);
|
||||
isActivePeriod: true,
|
||||
activeGuide: testGuideStep3ActiveState,
|
||||
});
|
||||
find('guideButton').simulate('click');
|
||||
component.update();
|
||||
|
||||
expect(
|
||||
find('guidePanelStepDescription')
|
||||
|
@ -357,10 +400,14 @@ describe('Guided setup', () => {
|
|||
).toBe(true);
|
||||
});
|
||||
|
||||
test('should render the step description as an unordered list if it is more than one sentence', async () => {
|
||||
const { component, find } = testBed;
|
||||
|
||||
await updateComponentWithState(component, mockActiveTestGuideState, true);
|
||||
test('renders the step description list as an unordered list', async () => {
|
||||
const { find, component } = await setupComponentWithPluginStateMock(httpClient, {
|
||||
status: 'in_progress',
|
||||
isActivePeriod: true,
|
||||
activeGuide: testGuideStep1ActiveState,
|
||||
});
|
||||
find('guideButton').simulate('click');
|
||||
component.update();
|
||||
|
||||
expect(
|
||||
find('guidePanelStepDescription')
|
||||
|
@ -378,13 +425,14 @@ describe('Guided setup', () => {
|
|||
|
||||
describe('Quit guide modal', () => {
|
||||
beforeEach(async () => {
|
||||
const { component, find, exists } = testBed;
|
||||
|
||||
await act(async () => {
|
||||
// Enable the "test" guide
|
||||
await apiService.updateGuideState(mockActiveTestGuideState, true);
|
||||
testBed = await setupComponentWithPluginStateMock(httpClient, {
|
||||
status: 'in_progress',
|
||||
isActivePeriod: true,
|
||||
activeGuide: testGuideStep1ActiveState,
|
||||
});
|
||||
|
||||
const { find, component, exists } = testBed;
|
||||
find('guideButton').simulate('click');
|
||||
component.update();
|
||||
|
||||
await act(async () => {
|
||||
|
@ -406,11 +454,15 @@ describe('Guided setup', () => {
|
|||
component.update();
|
||||
|
||||
expect(exists('onboarding--quitGuideModal')).toBe(false);
|
||||
|
||||
// TODO check for the correct button behavior once https://github.com/elastic/kibana/issues/141129 is implemented
|
||||
});
|
||||
|
||||
test('cancels out of the quit guide confirmation modal', async () => {
|
||||
httpClient.put.mockResolvedValueOnce({
|
||||
pluginState: {
|
||||
status: 'quit',
|
||||
isActivePeriod: true,
|
||||
},
|
||||
});
|
||||
const { component, find, exists } = testBed;
|
||||
|
||||
await act(async () => {
|
||||
|
|
|
@ -32,9 +32,9 @@ import { ApplicationStart } from '@kbn/core/public';
|
|||
import type { GuideState, GuideStep as GuideStepStatus } from '@kbn/guided-onboarding';
|
||||
|
||||
import { GuideId } from '@kbn/guided-onboarding';
|
||||
import type { GuideConfig, StepConfig } from '../types';
|
||||
import type { GuideConfig, GuidedOnboardingApi, StepConfig } from '../types';
|
||||
|
||||
import type { ApiService } from '../services/api';
|
||||
import type { PluginState } from '../../common/types';
|
||||
import { getGuideConfig } from '../services/helpers';
|
||||
|
||||
import { GuideStep } from './guide_panel_step';
|
||||
|
@ -43,7 +43,7 @@ import { getGuidePanelStyles } from './guide_panel.styles';
|
|||
import { GuideButton } from './guide_button';
|
||||
|
||||
interface GuidePanelProps {
|
||||
api: ApiService;
|
||||
api: GuidedOnboardingApi;
|
||||
application: ApplicationStart;
|
||||
}
|
||||
|
||||
|
@ -61,7 +61,7 @@ const getProgress = (state?: GuideState): number => {
|
|||
|
||||
// Temporarily provide a different guide ID for telemetry purposes
|
||||
// Should not be necessary once https://github.com/elastic/kibana/issues/144452 is addressed
|
||||
const getTelemetryGuideId = (guideId: GuideId) => {
|
||||
const getTelemetryGuideId = (guideId?: GuideId) => {
|
||||
switch (guideId) {
|
||||
case 'security':
|
||||
return 'siem';
|
||||
|
@ -77,7 +77,7 @@ export const GuidePanel = ({ api, application }: GuidePanelProps) => {
|
|||
const { euiTheme } = useEuiTheme();
|
||||
const [isGuideOpen, setIsGuideOpen] = useState(false);
|
||||
const [isQuitGuideModalOpen, setIsQuitGuideModalOpen] = useState(false);
|
||||
const [guideState, setGuideState] = useState<GuideState | undefined>(undefined);
|
||||
const [pluginState, setPluginState] = useState<PluginState | undefined>(undefined);
|
||||
|
||||
const styles = getGuidePanelStyles(euiTheme);
|
||||
|
||||
|
@ -86,15 +86,16 @@ export const GuidePanel = ({ api, application }: GuidePanelProps) => {
|
|||
};
|
||||
|
||||
const handleStepButtonClick = async (step: GuideStepStatus, stepConfig: StepConfig) => {
|
||||
if (guideState) {
|
||||
if (pluginState) {
|
||||
const { id, status } = step;
|
||||
const guideId: GuideId = pluginState!.activeGuide!.guideId!;
|
||||
|
||||
if (status === 'ready_to_complete') {
|
||||
return await api.completeGuideStep(guideState?.guideId, id);
|
||||
return await api.completeGuideStep(guideId, id);
|
||||
}
|
||||
|
||||
if (status === 'active' || status === 'in_progress') {
|
||||
await api.startGuideStep(guideState!.guideId, id);
|
||||
await api.startGuideStep(guideId, id);
|
||||
|
||||
if (stepConfig.location) {
|
||||
await application.navigateToApp(stepConfig.location.appID, {
|
||||
|
@ -102,7 +103,7 @@ export const GuidePanel = ({ api, application }: GuidePanelProps) => {
|
|||
});
|
||||
|
||||
if (stepConfig.manualCompletion?.readyToCompleteOnNavigation) {
|
||||
await api.completeGuideStep(guideState.guideId, id);
|
||||
await api.completeGuideStep(guideId, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -117,7 +118,7 @@ export const GuidePanel = ({ api, application }: GuidePanelProps) => {
|
|||
const completeGuide = async (
|
||||
completedGuideRedirectLocation: GuideConfig['completedGuideRedirectLocation']
|
||||
) => {
|
||||
await api.completeGuide(guideState!.guideId);
|
||||
await api.completeGuide(pluginState!.activeGuide!.guideId!);
|
||||
|
||||
if (completedGuideRedirectLocation) {
|
||||
const { appID, path } = completedGuideRedirectLocation;
|
||||
|
@ -137,8 +138,8 @@ export const GuidePanel = ({ api, application }: GuidePanelProps) => {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = api.fetchActiveGuideState$().subscribe((newGuideState) => {
|
||||
setGuideState(newGuideState);
|
||||
const subscription = api.fetchPluginState$().subscribe((newPluginState) => {
|
||||
setPluginState(newPluginState);
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
}, [api]);
|
||||
|
@ -150,25 +151,22 @@ export const GuidePanel = ({ api, application }: GuidePanelProps) => {
|
|||
return () => subscription.unsubscribe();
|
||||
}, [api]);
|
||||
|
||||
const guideConfig = getGuideConfig(guideState?.guideId);
|
||||
const guideConfig = getGuideConfig(pluginState?.activeGuide?.guideId)!;
|
||||
|
||||
// TODO handle loading, error state
|
||||
// https://github.com/elastic/kibana/issues/139799, https://github.com/elastic/kibana/issues/139798
|
||||
if (!guideConfig || !guideState || !guideState.isActive) {
|
||||
// TODO button show/hide logic https://github.com/elastic/kibana/issues/141129
|
||||
return null;
|
||||
}
|
||||
|
||||
const stepsCompleted = getProgress(guideState);
|
||||
const isGuideReadyToComplete = guideState?.status === 'ready_to_complete';
|
||||
const telemetryGuideId = getTelemetryGuideId(guideState.guideId);
|
||||
const stepsCompleted = getProgress(pluginState?.activeGuide);
|
||||
const isGuideReadyToComplete = pluginState?.activeGuide?.status === 'ready_to_complete';
|
||||
const telemetryGuideId = getTelemetryGuideId(pluginState?.activeGuide?.guideId);
|
||||
|
||||
return (
|
||||
<>
|
||||
<GuideButton
|
||||
guideState={guideState!}
|
||||
pluginState={pluginState}
|
||||
toggleGuidePanel={toggleGuide}
|
||||
isGuidePanelOpen={isGuideOpen}
|
||||
navigateToLandingPage={navigateToLandingPage}
|
||||
/>
|
||||
|
||||
{isGuideOpen && (
|
||||
|
@ -270,7 +268,7 @@ export const GuidePanel = ({ api, application }: GuidePanelProps) => {
|
|||
|
||||
{guideConfig?.steps.map((step, index) => {
|
||||
const accordionId = htmlIdGenerator(`accordion${index}`)();
|
||||
const stepState = guideState?.steps[index];
|
||||
const stepState = pluginState?.activeGuide?.steps[index];
|
||||
|
||||
if (stepState) {
|
||||
return (
|
||||
|
@ -281,7 +279,7 @@ export const GuidePanel = ({ api, application }: GuidePanelProps) => {
|
|||
stepNumber={index + 1}
|
||||
handleButtonClick={() => handleStepButtonClick(stepState, step)}
|
||||
key={accordionId}
|
||||
telemetryGuideId={telemetryGuideId}
|
||||
telemetryGuideId={telemetryGuideId!}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -374,8 +372,8 @@ export const GuidePanel = ({ api, application }: GuidePanelProps) => {
|
|||
{isQuitGuideModalOpen && (
|
||||
<QuitGuideModal
|
||||
closeModal={closeQuitGuideModal}
|
||||
currentGuide={guideState!}
|
||||
telemetryGuideId={telemetryGuideId}
|
||||
currentGuide={pluginState!.activeGuide!}
|
||||
telemetryGuideId={telemetryGuideId!}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -12,6 +12,10 @@ export const testGuideConfig: GuideConfig = {
|
|||
title: 'Test guide for development',
|
||||
description: `This guide is used to test the guided onboarding UI while in development and to run automated tests for the API and UI components.`,
|
||||
guideName: 'Testing example',
|
||||
completedGuideRedirectLocation: {
|
||||
appID: 'guidedOnboardingExample',
|
||||
path: '/',
|
||||
},
|
||||
docs: {
|
||||
text: 'Testing example docs',
|
||||
url: 'example.com',
|
||||
|
|
|
@ -12,16 +12,18 @@ import { GuidedOnboardingPluginStart } from '.';
|
|||
const apiServiceMock: jest.Mocked<GuidedOnboardingPluginStart> = {
|
||||
guidedOnboardingApi: {
|
||||
setup: jest.fn(),
|
||||
fetchActiveGuideState$: () => new BehaviorSubject(undefined),
|
||||
fetchPluginState$: () => new BehaviorSubject(undefined),
|
||||
fetchAllGuidesState: jest.fn(),
|
||||
updateGuideState: jest.fn(),
|
||||
updatePluginState: jest.fn(),
|
||||
activateGuide: jest.fn(),
|
||||
deactivateGuide: jest.fn(),
|
||||
completeGuide: jest.fn(),
|
||||
isGuideStepActive$: () => new BehaviorSubject(false),
|
||||
startGuideStep: jest.fn(),
|
||||
completeGuideStep: jest.fn(),
|
||||
isGuidedOnboardingActiveForIntegration$: () => new BehaviorSubject(false),
|
||||
completeGuidedOnboardingForIntegration: jest.fn(),
|
||||
skipGuidedOnboarding: jest.fn(),
|
||||
isGuidePanelOpen$: new BehaviorSubject(false),
|
||||
},
|
||||
};
|
|
@ -36,7 +36,7 @@ export class GuidedOnboardingPlugin
|
|||
const { chrome, http, theme, application } = core;
|
||||
|
||||
// Initialize services
|
||||
apiService.setup(http);
|
||||
apiService.setup(http, !!cloud?.isCloudEnabled);
|
||||
|
||||
// Guided onboarding UI is only available on cloud
|
||||
if (cloud?.isCloudEnabled) {
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
import type { GuideState, GuideId, GuideStepIds } from '@kbn/guided-onboarding';
|
||||
|
||||
import { PluginState } from '../../common/types';
|
||||
|
||||
export const testGuide: GuideId = 'testGuide';
|
||||
export const testGuideFirstStep: GuideStepIds = 'step1';
|
||||
export const testGuideManualCompletionStep = 'step2';
|
||||
|
@ -77,6 +79,39 @@ export const testGuideStep2InProgressState: GuideState = {
|
|||
],
|
||||
};
|
||||
|
||||
export const testGuideStep2ReadyToCompleteState: GuideState = {
|
||||
...testGuideStep1ActiveState,
|
||||
steps: [
|
||||
{
|
||||
...testGuideStep1ActiveState.steps[0],
|
||||
status: 'complete',
|
||||
},
|
||||
{
|
||||
id: testGuideStep1ActiveState.steps[1].id,
|
||||
status: 'ready_to_complete',
|
||||
},
|
||||
testGuideStep1ActiveState.steps[2],
|
||||
],
|
||||
};
|
||||
|
||||
export const testGuideStep3ActiveState: GuideState = {
|
||||
...testGuideStep1ActiveState,
|
||||
steps: [
|
||||
{
|
||||
...testGuideStep1ActiveState.steps[0],
|
||||
status: 'complete',
|
||||
},
|
||||
{
|
||||
id: testGuideStep1ActiveState.steps[1].id,
|
||||
status: 'complete',
|
||||
},
|
||||
{
|
||||
id: testGuideStep1ActiveState.steps[2].id,
|
||||
status: 'active',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const readyToCompleteGuideState: GuideState = {
|
||||
...testGuideStep1ActiveState,
|
||||
steps: [
|
||||
|
@ -99,3 +134,14 @@ export const testGuideNotActiveState: GuideState = {
|
|||
...testGuideStep1ActiveState,
|
||||
isActive: false,
|
||||
};
|
||||
|
||||
export const mockPluginStateNotStarted: PluginState = {
|
||||
status: 'not_started',
|
||||
isActivePeriod: true,
|
||||
};
|
||||
|
||||
export const mockPluginStateInProgress: PluginState = {
|
||||
status: 'in_progress',
|
||||
isActivePeriod: true,
|
||||
activeGuide: testGuideStep1ActiveState,
|
||||
};
|
||||
|
|
|
@ -11,7 +11,6 @@ import { httpServiceMock } from '@kbn/core/public/mocks';
|
|||
import type { GuideState } from '@kbn/guided-onboarding';
|
||||
import { firstValueFrom, Subscription } from 'rxjs';
|
||||
|
||||
import { GuideStatus } from '@kbn/guided-onboarding';
|
||||
import { API_BASE_PATH } from '../../common/constants';
|
||||
import { ApiService } from './api';
|
||||
import {
|
||||
|
@ -27,6 +26,9 @@ import {
|
|||
wrongIntegration,
|
||||
testGuideStep2InProgressState,
|
||||
readyToCompleteGuideState,
|
||||
mockPluginStateInProgress,
|
||||
mockPluginStateNotStarted,
|
||||
testGuideStep3ActiveState,
|
||||
} from './api.mocks';
|
||||
|
||||
describe('GuidedOnboarding ApiService', () => {
|
||||
|
@ -38,10 +40,13 @@ describe('GuidedOnboarding ApiService', () => {
|
|||
beforeEach(() => {
|
||||
httpClient = httpServiceMock.createStartContract({ basePath: '/base/path' });
|
||||
httpClient.get.mockResolvedValue({
|
||||
state: [testGuideStep1ActiveState],
|
||||
pluginState: mockPluginStateInProgress,
|
||||
});
|
||||
httpClient.put.mockResolvedValue({
|
||||
pluginState: mockPluginStateInProgress,
|
||||
});
|
||||
apiService = new ApiService();
|
||||
apiService.setup(httpClient);
|
||||
apiService.setup(httpClient, true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -50,86 +55,107 @@ describe('GuidedOnboarding ApiService', () => {
|
|||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('fetchActiveGuideState$', () => {
|
||||
it('sends a request to the get API', () => {
|
||||
subscription = apiService.fetchActiveGuideState$().subscribe();
|
||||
describe('fetchPluginState$', () => {
|
||||
it('sends a request to the get state API', () => {
|
||||
subscription = apiService.fetchPluginState$().subscribe();
|
||||
expect(httpClient.get).toHaveBeenCalledTimes(1);
|
||||
expect(httpClient.get).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
|
||||
query: { active: true },
|
||||
signal: new AbortController().signal,
|
||||
});
|
||||
});
|
||||
|
||||
it(`doesn't send multiple requests when there are several subscriptions`, () => {
|
||||
subscription = apiService.fetchActiveGuideState$().subscribe();
|
||||
anotherSubscription = apiService.fetchActiveGuideState$().subscribe();
|
||||
subscription = apiService.fetchPluginState$().subscribe();
|
||||
anotherSubscription = apiService.fetchPluginState$().subscribe();
|
||||
expect(httpClient.get).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it(`re-sends the request if the previous one failed`, async () => {
|
||||
httpClient.get.mockRejectedValueOnce(new Error('request failed'));
|
||||
subscription = apiService.fetchActiveGuideState$().subscribe();
|
||||
subscription = apiService.fetchPluginState$().subscribe();
|
||||
// wait until the request fails
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
anotherSubscription = apiService.fetchActiveGuideState$().subscribe();
|
||||
anotherSubscription = apiService.fetchPluginState$().subscribe();
|
||||
expect(httpClient.get).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it(`doesn't re-send the request if there is no guide state and there is another subscription`, async () => {
|
||||
httpClient.get.mockResolvedValueOnce({
|
||||
state: [],
|
||||
});
|
||||
subscription = apiService.fetchActiveGuideState$().subscribe();
|
||||
// wait until the request completes
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
anotherSubscription = apiService.fetchActiveGuideState$().subscribe();
|
||||
expect(httpClient.get).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it(`doesn't send multiple requests in a loop when there is no state`, async () => {
|
||||
httpClient.get.mockResolvedValueOnce({
|
||||
state: [],
|
||||
});
|
||||
subscription = apiService.fetchActiveGuideState$().subscribe();
|
||||
// wait until the request completes
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
expect(httpClient.get).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it(`re-sends the request if the subscription was unsubscribed before the request completed`, async () => {
|
||||
httpClient.get.mockImplementationOnce(() => {
|
||||
return new Promise((resolve) => setTimeout(resolve));
|
||||
});
|
||||
// subscribe and immediately unsubscribe
|
||||
apiService.fetchActiveGuideState$().subscribe().unsubscribe();
|
||||
anotherSubscription = apiService.fetchActiveGuideState$().subscribe();
|
||||
apiService.fetchPluginState$().subscribe().unsubscribe();
|
||||
anotherSubscription = apiService.fetchPluginState$().subscribe();
|
||||
expect(httpClient.get).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it(`the second subscription gets the state broadcast to it`, (done) => {
|
||||
// first subscription
|
||||
apiService.fetchActiveGuideState$().subscribe();
|
||||
apiService.fetchPluginState$().subscribe();
|
||||
// second subscription
|
||||
anotherSubscription = apiService.fetchActiveGuideState$().subscribe((state) => {
|
||||
anotherSubscription = apiService.fetchPluginState$().subscribe((state) => {
|
||||
if (state) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('broadcasts the updated state', async () => {
|
||||
await apiService.activateGuide(testGuide, testGuideStep1ActiveState);
|
||||
|
||||
const state = await firstValueFrom(apiService.fetchActiveGuideState$());
|
||||
expect(state).toEqual(testGuideStep1ActiveState);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchAllGuidesState', () => {
|
||||
it('sends a request to the get API', async () => {
|
||||
it('sends a request to the get guide API', async () => {
|
||||
await apiService.fetchAllGuidesState();
|
||||
expect(httpClient.get).toHaveBeenCalledTimes(1);
|
||||
expect(httpClient.get).toHaveBeenCalledWith(`${API_BASE_PATH}/state`);
|
||||
expect(httpClient.get).toHaveBeenCalledWith(`${API_BASE_PATH}/guides`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updatePluginState', () => {
|
||||
it('sends a request to the put state API when updating the guide', async () => {
|
||||
await apiService.updatePluginState({ guide: testGuideStep1InProgressState }, false);
|
||||
expect(httpClient.put).toHaveBeenCalledTimes(1);
|
||||
expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
|
||||
body: JSON.stringify({ guide: testGuideStep1InProgressState }),
|
||||
});
|
||||
});
|
||||
|
||||
it('sends a request to the put state API when updating the status', async () => {
|
||||
await apiService.updatePluginState({ status: 'quit' }, false);
|
||||
expect(httpClient.put).toHaveBeenCalledTimes(1);
|
||||
expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
|
||||
body: JSON.stringify({ status: 'quit' }),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('activateGuide', () => {
|
||||
it('activates a new guide', async () => {
|
||||
// update the mock to no active guides
|
||||
httpClient.get.mockResolvedValue({
|
||||
pluginState: mockPluginStateNotStarted,
|
||||
});
|
||||
apiService.setup(httpClient, true);
|
||||
|
||||
await apiService.activateGuide(testGuide);
|
||||
|
||||
expect(httpClient.put).toHaveBeenCalledTimes(1);
|
||||
expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
|
||||
body: JSON.stringify({
|
||||
status: 'in_progress',
|
||||
guide: { ...testGuideStep1ActiveState, status: 'not_started' },
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('reactivates a guide that has already been started', async () => {
|
||||
await apiService.activateGuide(testGuide, testGuideStep1ActiveState);
|
||||
|
||||
expect(httpClient.put).toHaveBeenCalledTimes(1);
|
||||
expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
|
||||
body: JSON.stringify({
|
||||
status: 'in_progress',
|
||||
guide: testGuideStep1ActiveState,
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -140,124 +166,53 @@ describe('GuidedOnboarding ApiService', () => {
|
|||
expect(httpClient.put).toHaveBeenCalledTimes(1);
|
||||
expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
|
||||
body: JSON.stringify({
|
||||
...testGuideStep1ActiveState,
|
||||
isActive: false,
|
||||
status: 'quit',
|
||||
guide: {
|
||||
...testGuideStep1ActiveState,
|
||||
isActive: false,
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateGuideState', () => {
|
||||
it('sends a request to the put API', async () => {
|
||||
const updatedState: GuideState = testGuideStep1InProgressState;
|
||||
await apiService.updateGuideState(updatedState, false);
|
||||
expect(httpClient.put).toHaveBeenCalledTimes(1);
|
||||
expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
|
||||
body: JSON.stringify(updatedState),
|
||||
});
|
||||
});
|
||||
|
||||
it('the completed state is being broadcast after the update', async () => {
|
||||
const completedState = {
|
||||
...readyToCompleteGuideState,
|
||||
isActive: false,
|
||||
status: 'complete' as GuideStatus,
|
||||
};
|
||||
await apiService.updateGuideState(completedState, false);
|
||||
const state = await firstValueFrom(apiService.fetchActiveGuideState$());
|
||||
expect(state).toMatchObject(completedState);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isGuideStepActive$', () => {
|
||||
it('returns true if the step has been started', (done) => {
|
||||
httpClient.get.mockResolvedValueOnce({
|
||||
state: [testGuideStep1InProgressState],
|
||||
});
|
||||
|
||||
subscription = apiService
|
||||
.isGuideStepActive$(testGuide, testGuideFirstStep)
|
||||
.subscribe((isStepActive) => {
|
||||
if (isStepActive) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('returns false if the step is not been started', (done) => {
|
||||
subscription = apiService
|
||||
.isGuideStepActive$(testGuide, testGuideFirstStep)
|
||||
.subscribe((isStepActive) => {
|
||||
if (!isStepActive) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it(`doesn't duplicate requests when there are several subscriptions and no guide state`, async () => {
|
||||
httpClient.get.mockResolvedValue({
|
||||
state: [],
|
||||
});
|
||||
apiService.setup(httpClient);
|
||||
|
||||
subscription = apiService.isGuideStepActive$(testGuide, testGuideFirstStep).subscribe();
|
||||
|
||||
// wait for the get request to resolve
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
anotherSubscription = apiService
|
||||
.isGuideStepActive$(testGuide, testGuideFirstStep)
|
||||
.subscribe();
|
||||
|
||||
expect(httpClient.get).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('activateGuide', () => {
|
||||
it('activates a new guide', async () => {
|
||||
// update the mock to no active guides
|
||||
httpClient.get.mockResolvedValue({
|
||||
state: [],
|
||||
});
|
||||
apiService.setup(httpClient);
|
||||
|
||||
await apiService.activateGuide(testGuide);
|
||||
|
||||
expect(httpClient.put).toHaveBeenCalledTimes(1);
|
||||
expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
|
||||
body: JSON.stringify({ ...testGuideStep1ActiveState, status: 'not_started' }),
|
||||
});
|
||||
});
|
||||
|
||||
it('reactivates a guide that has already been started', async () => {
|
||||
await apiService.activateGuide(testGuide, testGuideStep1ActiveState);
|
||||
|
||||
expect(httpClient.put).toHaveBeenCalledTimes(1);
|
||||
expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
|
||||
body: JSON.stringify(testGuideStep1ActiveState),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('completeGuide', () => {
|
||||
beforeEach(async () => {
|
||||
httpClient.get.mockResolvedValue({
|
||||
state: [readyToCompleteGuideState],
|
||||
pluginState: {
|
||||
...mockPluginStateInProgress,
|
||||
activeGuide: readyToCompleteGuideState,
|
||||
},
|
||||
});
|
||||
apiService.setup(httpClient);
|
||||
apiService.setup(httpClient, true);
|
||||
});
|
||||
|
||||
it('updates the selected guide and marks it as complete', async () => {
|
||||
await apiService.completeGuide(testGuide);
|
||||
|
||||
expect(httpClient.put).toHaveBeenCalledTimes(1);
|
||||
expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
|
||||
body: JSON.stringify({
|
||||
...readyToCompleteGuideState,
|
||||
isActive: false,
|
||||
status: 'complete',
|
||||
guide: {
|
||||
...readyToCompleteGuideState,
|
||||
isActive: false,
|
||||
status: 'complete',
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('the completed state is being broadcast after the update', async () => {
|
||||
httpClient.put.mockResolvedValueOnce({
|
||||
// mock the put api response
|
||||
pluginState: { status: 'complete', isActivePeriod: true },
|
||||
});
|
||||
await apiService.completeGuide(testGuide);
|
||||
const updateState = await firstValueFrom(apiService.fetchPluginState$());
|
||||
expect(updateState?.status).toBe('complete');
|
||||
});
|
||||
|
||||
it('returns undefined if the selected guide is not active', async () => {
|
||||
const completedState = await apiService.completeGuide('observability'); // not active
|
||||
expect(completedState).not.toBeDefined();
|
||||
|
@ -284,25 +239,44 @@ describe('GuidedOnboarding ApiService', () => {
|
|||
httpClient.get.mockResolvedValue({
|
||||
state: [incompleteGuideState],
|
||||
});
|
||||
apiService.setup(httpClient);
|
||||
apiService.setup(httpClient, true);
|
||||
const completedState = await apiService.completeGuide(testGuide);
|
||||
expect(completedState).not.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('startGuideStep', () => {
|
||||
beforeEach(async () => {
|
||||
httpClient.get.mockResolvedValue({
|
||||
state: [testGuideStep1ActiveState],
|
||||
describe('isGuideStepActive$', () => {
|
||||
it('returns true if the step has been started', (done) => {
|
||||
httpClient.get.mockResolvedValueOnce({
|
||||
pluginState: { ...mockPluginStateInProgress, activeGuide: testGuideStep1InProgressState },
|
||||
});
|
||||
apiService.setup(httpClient);
|
||||
|
||||
subscription = apiService
|
||||
.isGuideStepActive$(testGuide, testGuideFirstStep)
|
||||
.subscribe((isStepActive) => {
|
||||
if (isStepActive) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('returns false if the step is not been started', (done) => {
|
||||
subscription = apiService
|
||||
.isGuideStepActive$(testGuide, testGuideFirstStep)
|
||||
.subscribe((isStepActive) => {
|
||||
if (!isStepActive) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('startGuideStep', () => {
|
||||
it('updates the selected step and marks it as in_progress', async () => {
|
||||
await apiService.startGuideStep(testGuide, testGuideFirstStep);
|
||||
|
||||
expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
|
||||
body: JSON.stringify(testGuideStep1InProgressState),
|
||||
body: JSON.stringify({ guide: testGuideStep1InProgressState }),
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -315,24 +289,24 @@ describe('GuidedOnboarding ApiService', () => {
|
|||
describe('completeGuideStep', () => {
|
||||
it(`completes the step when it's in progress`, async () => {
|
||||
httpClient.get.mockResolvedValue({
|
||||
state: [testGuideStep1InProgressState],
|
||||
pluginState: { ...mockPluginStateInProgress, activeGuide: testGuideStep1InProgressState },
|
||||
});
|
||||
apiService.setup(httpClient);
|
||||
apiService.setup(httpClient, true);
|
||||
|
||||
await apiService.completeGuideStep(testGuide, testGuideFirstStep);
|
||||
|
||||
expect(httpClient.put).toHaveBeenCalledTimes(1);
|
||||
// Verify the completed step now has a "complete" status, and the subsequent step is "active"
|
||||
expect(httpClient.put).toHaveBeenLastCalledWith(`${API_BASE_PATH}/state`, {
|
||||
body: JSON.stringify({ ...testGuideStep2ActiveState }),
|
||||
body: JSON.stringify({ guide: { ...testGuideStep2ActiveState } }),
|
||||
});
|
||||
});
|
||||
|
||||
it(`marks the step as 'ready_to_complete' if it's configured for manual completion`, async () => {
|
||||
httpClient.get.mockResolvedValue({
|
||||
state: [testGuideStep2InProgressState],
|
||||
pluginState: { ...mockPluginStateInProgress, activeGuide: testGuideStep2InProgressState },
|
||||
});
|
||||
apiService.setup(httpClient);
|
||||
apiService.setup(httpClient, true);
|
||||
|
||||
await apiService.completeGuideStep(testGuide, testGuideManualCompletionStep);
|
||||
|
||||
|
@ -340,19 +314,76 @@ describe('GuidedOnboarding ApiService', () => {
|
|||
// Verify the completed step now has a "ready_to_complete" status, and the subsequent step is "inactive"
|
||||
expect(httpClient.put).toHaveBeenLastCalledWith(`${API_BASE_PATH}/state`, {
|
||||
body: JSON.stringify({
|
||||
...testGuideStep2InProgressState,
|
||||
steps: [
|
||||
testGuideStep2InProgressState.steps[0],
|
||||
{ ...testGuideStep2InProgressState.steps[1], status: 'ready_to_complete' },
|
||||
testGuideStep2InProgressState.steps[2],
|
||||
],
|
||||
guide: {
|
||||
...testGuideStep2InProgressState,
|
||||
steps: [
|
||||
testGuideStep2InProgressState.steps[0],
|
||||
{ ...testGuideStep2InProgressState.steps[1], status: 'ready_to_complete' },
|
||||
testGuideStep2InProgressState.steps[2],
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns undefined if the selected guide is not active', async () => {
|
||||
const startState = await apiService.completeGuideStep('observability', 'add_data'); // not active
|
||||
expect(startState).not.toBeDefined();
|
||||
it('marks the guide as "ready_to_complete" if the current step is the last step in the guide and configured for manual completion', async () => {
|
||||
httpClient.get.mockResolvedValue({
|
||||
pluginState: {
|
||||
...mockPluginStateInProgress,
|
||||
activeGuide: {
|
||||
...testGuideStep3ActiveState,
|
||||
steps: [
|
||||
...testGuideStep3ActiveState.steps.slice(0, 2),
|
||||
{ ...testGuideStep3ActiveState.steps[2], status: 'ready_to_complete' },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
apiService.setup(httpClient, true);
|
||||
|
||||
await apiService.completeGuideStep(testGuide, testGuideLastStep);
|
||||
|
||||
expect(httpClient.put).toHaveBeenCalledTimes(1);
|
||||
// Verify the guide now has a "ready_to_complete" status and the last step is "complete"
|
||||
expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
|
||||
body: JSON.stringify({
|
||||
guide: {
|
||||
...testGuideStep3ActiveState,
|
||||
status: 'ready_to_complete',
|
||||
steps: [
|
||||
...testGuideStep3ActiveState.steps.slice(0, 2),
|
||||
{ ...testGuideStep3ActiveState.steps[2], status: 'complete' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('marks the guide as "in_progress" if the current step is not the last step in the guide', async () => {
|
||||
httpClient.get.mockResolvedValue({
|
||||
pluginState: {
|
||||
...mockPluginStateInProgress,
|
||||
activeGuide: testGuideStep1InProgressState,
|
||||
},
|
||||
});
|
||||
apiService.setup(httpClient, true);
|
||||
|
||||
await apiService.completeGuideStep(testGuide, testGuideFirstStep);
|
||||
|
||||
expect(httpClient.put).toHaveBeenCalledTimes(1);
|
||||
// Verify the guide now has a "in_progress" status and the second step is "active"
|
||||
expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
|
||||
body: JSON.stringify({
|
||||
guide: {
|
||||
...testGuideStep3ActiveState,
|
||||
steps: [
|
||||
testGuideStep2ActiveState.steps[0],
|
||||
{ ...testGuideStep2ActiveState.steps[1], status: 'active' },
|
||||
testGuideStep2ActiveState.steps[2],
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('does nothing if the step is not in progress', async () => {
|
||||
|
@ -361,66 +392,18 @@ describe('GuidedOnboarding ApiService', () => {
|
|||
expect(httpClient.put).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('marks the guide as "ready_to_complete" if the current step is the last step in the guide and configured for manual completion', async () => {
|
||||
const testGuideStep3InProgressState: GuideState = {
|
||||
...testGuideStep2ActiveState,
|
||||
steps: [
|
||||
testGuideStep2ActiveState.steps[0],
|
||||
{ ...testGuideStep2ActiveState.steps[1], status: 'complete' },
|
||||
{ ...testGuideStep2ActiveState.steps[2], status: 'ready_to_complete' },
|
||||
],
|
||||
};
|
||||
httpClient.get.mockResolvedValue({
|
||||
state: [testGuideStep3InProgressState],
|
||||
});
|
||||
apiService.setup(httpClient);
|
||||
|
||||
await apiService.completeGuideStep(testGuide, testGuideLastStep);
|
||||
|
||||
expect(httpClient.put).toHaveBeenCalledTimes(1);
|
||||
// Verify the guide now has a "ready_to_complete" status and the last step is "complete"
|
||||
expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
|
||||
body: JSON.stringify({
|
||||
...testGuideStep3InProgressState,
|
||||
steps: [
|
||||
...testGuideStep3InProgressState.steps.slice(0, 2),
|
||||
{ ...testGuideStep3InProgressState.steps[2], status: 'complete' },
|
||||
],
|
||||
status: 'ready_to_complete',
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('marks the guide as "in_progress" if the current step is not the last step in the guide', async () => {
|
||||
httpClient.get.mockResolvedValue({
|
||||
state: [testGuideStep1InProgressState],
|
||||
});
|
||||
apiService.setup(httpClient);
|
||||
|
||||
await apiService.completeGuideStep(testGuide, testGuideFirstStep);
|
||||
|
||||
expect(httpClient.put).toHaveBeenCalledTimes(1);
|
||||
// Verify the guide now has a "in_progress" status and the second step is "active"
|
||||
expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
|
||||
body: JSON.stringify({
|
||||
...testGuideStep2ActiveState,
|
||||
steps: [
|
||||
testGuideStep2ActiveState.steps[0],
|
||||
{ ...testGuideStep2ActiveState.steps[1], status: 'active' },
|
||||
testGuideStep2ActiveState.steps[2],
|
||||
],
|
||||
status: 'in_progress',
|
||||
}),
|
||||
});
|
||||
it('returns undefined if the selected guide is not active', async () => {
|
||||
const startState = await apiService.completeGuideStep('observability', 'add_data'); // not active
|
||||
expect(startState).not.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isGuidedOnboardingActiveForIntegration$', () => {
|
||||
it('returns true if the integration is part of the active step', (done) => {
|
||||
httpClient.get.mockResolvedValue({
|
||||
state: [testGuideStep1InProgressState],
|
||||
pluginState: { ...mockPluginStateInProgress, activeGuide: testGuideStep1InProgressState },
|
||||
});
|
||||
apiService.setup(httpClient);
|
||||
apiService.setup(httpClient, true);
|
||||
subscription = apiService
|
||||
.isGuidedOnboardingActiveForIntegration$(testIntegration)
|
||||
.subscribe((isIntegrationInGuideStep) => {
|
||||
|
@ -432,9 +415,9 @@ describe('GuidedOnboarding ApiService', () => {
|
|||
|
||||
it('returns false if the current step has a different integration', (done) => {
|
||||
httpClient.get.mockResolvedValue({
|
||||
state: [testGuideStep1InProgressState],
|
||||
pluginState: { ...mockPluginStateInProgress, activeGuide: testGuideStep1InProgressState },
|
||||
});
|
||||
apiService.setup(httpClient);
|
||||
apiService.setup(httpClient, true);
|
||||
subscription = apiService
|
||||
.isGuidedOnboardingActiveForIntegration$(wrongIntegration)
|
||||
.subscribe((isIntegrationInGuideStep) => {
|
||||
|
@ -446,9 +429,9 @@ describe('GuidedOnboarding ApiService', () => {
|
|||
|
||||
it('returns false if no guide is active', (done) => {
|
||||
httpClient.get.mockResolvedValue({
|
||||
state: [testGuideNotActiveState],
|
||||
pluginState: { ...mockPluginStateNotStarted, activeGuide: testGuideNotActiveState },
|
||||
});
|
||||
apiService.setup(httpClient);
|
||||
apiService.setup(httpClient, true);
|
||||
subscription = apiService
|
||||
.isGuidedOnboardingActiveForIntegration$(testIntegration)
|
||||
.subscribe((isIntegrationInGuideStep) => {
|
||||
|
@ -462,23 +445,23 @@ describe('GuidedOnboarding ApiService', () => {
|
|||
describe('completeGuidedOnboardingForIntegration', () => {
|
||||
it(`completes the step if it's active for the integration`, async () => {
|
||||
httpClient.get.mockResolvedValue({
|
||||
state: [testGuideStep1InProgressState],
|
||||
pluginState: { ...mockPluginStateInProgress, activeGuide: testGuideStep1InProgressState },
|
||||
});
|
||||
apiService.setup(httpClient);
|
||||
apiService.setup(httpClient, true);
|
||||
|
||||
await apiService.completeGuidedOnboardingForIntegration(testIntegration);
|
||||
expect(httpClient.put).toHaveBeenCalledTimes(1);
|
||||
// this assertion depends on the guides config
|
||||
expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
|
||||
body: JSON.stringify(testGuideStep2ActiveState),
|
||||
body: JSON.stringify({ guide: testGuideStep2ActiveState }),
|
||||
});
|
||||
});
|
||||
|
||||
it(`does nothing if the step has a different integration`, async () => {
|
||||
httpClient.get.mockResolvedValue({
|
||||
state: [testGuideStep1InProgressState],
|
||||
pluginState: { ...mockPluginStateInProgress, activeGuide: testGuideStep1InProgressState },
|
||||
});
|
||||
apiService.setup(httpClient);
|
||||
apiService.setup(httpClient, true);
|
||||
|
||||
await apiService.completeGuidedOnboardingForIntegration(wrongIntegration);
|
||||
expect(httpClient.put).not.toHaveBeenCalled();
|
||||
|
@ -486,12 +469,32 @@ describe('GuidedOnboarding ApiService', () => {
|
|||
|
||||
it(`does nothing if no guide is active`, async () => {
|
||||
httpClient.get.mockResolvedValue({
|
||||
state: [testGuideNotActiveState],
|
||||
pluginState: { ...mockPluginStateNotStarted, activeGuide: testGuideNotActiveState },
|
||||
});
|
||||
apiService.setup(httpClient);
|
||||
|
||||
await apiService.completeGuidedOnboardingForIntegration(testIntegration);
|
||||
expect(httpClient.put).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('no API requests are sent on self-managed deployments', () => {
|
||||
beforeEach(() => {
|
||||
apiService.setup(httpClient, false);
|
||||
});
|
||||
|
||||
it('fetchPluginState$', () => {
|
||||
subscription = apiService.fetchPluginState$().subscribe();
|
||||
expect(httpClient.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fetchAllGuidesState', async () => {
|
||||
await apiService.fetchAllGuidesState();
|
||||
expect(httpClient.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('updatePluginState', async () => {
|
||||
await apiService.updatePluginState({}, false);
|
||||
expect(httpClient.put).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import { HttpSetup } from '@kbn/core/public';
|
||||
import { BehaviorSubject, map, Observable, firstValueFrom, concat } from 'rxjs';
|
||||
import { BehaviorSubject, map, Observable, firstValueFrom, concat, of } from 'rxjs';
|
||||
import type { GuideState, GuideId, GuideStep, GuideStepIds } from '@kbn/guided-onboarding';
|
||||
|
||||
import { GuidedOnboardingApi } from '../types';
|
||||
|
@ -20,69 +20,72 @@ import {
|
|||
isIntegrationInGuideStep,
|
||||
isStepInProgress,
|
||||
isStepReadyToComplete,
|
||||
isGuideActive,
|
||||
} from './helpers';
|
||||
import { API_BASE_PATH } from '../../common/constants';
|
||||
import { PluginState, PluginStatus } from '../../common/types';
|
||||
|
||||
export class ApiService implements GuidedOnboardingApi {
|
||||
private isCloudEnabled: boolean | undefined;
|
||||
private client: HttpSetup | undefined;
|
||||
private guideState$!: BehaviorSubject<GuideState | undefined>;
|
||||
private isGuideStateLoading: boolean | undefined;
|
||||
private isGuideStateInitialized: boolean | undefined;
|
||||
private pluginState$!: BehaviorSubject<PluginState | undefined>;
|
||||
private isPluginStateLoading: boolean | undefined;
|
||||
public isGuidePanelOpen$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
|
||||
public setup(httpClient: HttpSetup): void {
|
||||
public setup(httpClient: HttpSetup, isCloudEnabled: boolean) {
|
||||
this.isCloudEnabled = isCloudEnabled;
|
||||
this.client = httpClient;
|
||||
this.guideState$ = new BehaviorSubject<GuideState | undefined>(undefined);
|
||||
this.pluginState$ = new BehaviorSubject<PluginState | undefined>(undefined);
|
||||
this.isGuidePanelOpen$ = new BehaviorSubject<boolean>(false);
|
||||
}
|
||||
|
||||
private createGetStateObservable(): Observable<GuideState | undefined> {
|
||||
return new Observable<GuideState | undefined>((observer) => {
|
||||
private createGetPluginStateObservable(): Observable<PluginState | undefined> {
|
||||
return new Observable<PluginState | undefined>((observer) => {
|
||||
const controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
this.isGuideStateLoading = true;
|
||||
this.client!.get<{ state: GuideState[] }>(`${API_BASE_PATH}/state`, {
|
||||
query: {
|
||||
active: true,
|
||||
},
|
||||
this.isPluginStateLoading = true;
|
||||
this.client!.get<{ pluginState: PluginState }>(`${API_BASE_PATH}/state`, {
|
||||
signal,
|
||||
})
|
||||
.then((response) => {
|
||||
this.isGuideStateInitialized = true;
|
||||
this.isGuideStateLoading = false;
|
||||
// There should only be 1 active guide
|
||||
const hasState = response.state.length === 1;
|
||||
if (hasState) {
|
||||
observer.next(response.state[0]);
|
||||
this.guideState$.next(response.state[0]);
|
||||
}
|
||||
.then(({ pluginState }) => {
|
||||
this.isPluginStateLoading = false;
|
||||
observer.next(pluginState);
|
||||
this.pluginState$.next(pluginState);
|
||||
observer.complete();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.isGuideStateLoading = false;
|
||||
this.isPluginStateLoading = false;
|
||||
observer.error(error);
|
||||
});
|
||||
return () => {
|
||||
this.isGuideStateLoading = false;
|
||||
this.isPluginStateLoading = false;
|
||||
controller.abort();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* An Observable with the active guide state.
|
||||
* An Observable with the plugin state.
|
||||
* Initially the state is fetched from the backend.
|
||||
* Subsequently, the observable is updated automatically, when the state changes.
|
||||
*/
|
||||
public fetchActiveGuideState$(): Observable<GuideState | undefined> {
|
||||
const currentState = this.guideState$.value;
|
||||
// if currentState is undefined, it can be because there is no active guide or we haven't fetched the data from the backend
|
||||
// check if there is no request in flight
|
||||
// also check if we have fetched the data from the backend already once, if yes no request is sent
|
||||
if (!currentState && !this.isGuideStateLoading && !this.isGuideStateInitialized) {
|
||||
this.isGuideStateLoading = true;
|
||||
return concat(this.createGetStateObservable(), this.guideState$);
|
||||
public fetchPluginState$(): Observable<PluginState | undefined> {
|
||||
if (!this.isCloudEnabled) {
|
||||
return of(undefined);
|
||||
}
|
||||
return this.guideState$;
|
||||
if (!this.client) {
|
||||
throw new Error('ApiService has not be initialized.');
|
||||
}
|
||||
|
||||
const currentState = this.pluginState$.value;
|
||||
// if currentState is undefined, it was not fetched from the backend yet
|
||||
// or the request was cancelled or failed
|
||||
// also check if we don't have a request in flight already
|
||||
if (!currentState && !this.isPluginStateLoading) {
|
||||
this.isPluginStateLoading = true;
|
||||
return concat(this.createGetPluginStateObservable(), this.pluginState$);
|
||||
}
|
||||
return this.pluginState$;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -91,12 +94,15 @@ export class ApiService implements GuidedOnboardingApi {
|
|||
* where all guides are displayed with their corresponding status
|
||||
*/
|
||||
public async fetchAllGuidesState(): Promise<{ state: GuideState[] } | undefined> {
|
||||
if (!this.isCloudEnabled) {
|
||||
return undefined;
|
||||
}
|
||||
if (!this.client) {
|
||||
throw new Error('ApiService has not be initialized.');
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.client.get<{ state: GuideState[] }>(`${API_BASE_PATH}/state`);
|
||||
return await this.client.get<{ state: GuideState[] }>(`${API_BASE_PATH}/guides`);
|
||||
} catch (error) {
|
||||
// TODO handle error
|
||||
// eslint-disable-next-line no-console
|
||||
|
@ -111,20 +117,26 @@ export class ApiService implements GuidedOnboardingApi {
|
|||
* @param {boolean} panelState boolean to determine whether the dropdown panel should open or not
|
||||
* @return {Promise} a promise with the updated guide state
|
||||
*/
|
||||
public async updateGuideState(
|
||||
newState: GuideState,
|
||||
public async updatePluginState(
|
||||
state: { status?: PluginStatus; guide?: GuideState },
|
||||
panelState: boolean
|
||||
): Promise<{ state: GuideState } | undefined> {
|
||||
): Promise<{ pluginState: PluginState } | undefined> {
|
||||
if (!this.isCloudEnabled) {
|
||||
return undefined;
|
||||
}
|
||||
if (!this.client) {
|
||||
throw new Error('ApiService has not be initialized.');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.client.put<{ state: GuideState }>(`${API_BASE_PATH}/state`, {
|
||||
body: JSON.stringify(newState),
|
||||
});
|
||||
// broadcast the newState
|
||||
this.guideState$.next(newState);
|
||||
const response = await this.client.put<{ pluginState: PluginState }>(
|
||||
`${API_BASE_PATH}/state`,
|
||||
{
|
||||
body: JSON.stringify(state),
|
||||
}
|
||||
);
|
||||
// update the guide state in the plugin state observable
|
||||
this.pluginState$.next(response.pluginState);
|
||||
this.isGuidePanelOpen$.next(panelState);
|
||||
return response;
|
||||
} catch (error) {
|
||||
|
@ -144,14 +156,17 @@ export class ApiService implements GuidedOnboardingApi {
|
|||
public async activateGuide(
|
||||
guideId: GuideId,
|
||||
guide?: GuideState
|
||||
): Promise<{ state: GuideState } | undefined> {
|
||||
): Promise<{ pluginState: PluginState } | undefined> {
|
||||
// If we already have the guide state (i.e., user has already started the guide at some point),
|
||||
// simply pass it through so they can continue where they left off, and update the guide to active
|
||||
if (guide) {
|
||||
return await this.updateGuideState(
|
||||
return await this.updatePluginState(
|
||||
{
|
||||
...guide,
|
||||
isActive: true,
|
||||
status: 'in_progress',
|
||||
guide: {
|
||||
...guide,
|
||||
isActive: true,
|
||||
},
|
||||
},
|
||||
true
|
||||
);
|
||||
|
@ -177,7 +192,13 @@ export class ApiService implements GuidedOnboardingApi {
|
|||
steps: updatedSteps,
|
||||
};
|
||||
|
||||
return await this.updateGuideState(updatedGuide, true);
|
||||
return await this.updatePluginState(
|
||||
{
|
||||
status: 'in_progress',
|
||||
guide: updatedGuide,
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -187,11 +208,16 @@ export class ApiService implements GuidedOnboardingApi {
|
|||
* @param {GuideState} guide the selected guide state
|
||||
* @return {Promise} a promise with the updated guide state
|
||||
*/
|
||||
public async deactivateGuide(guide: GuideState): Promise<{ state: GuideState } | undefined> {
|
||||
return await this.updateGuideState(
|
||||
public async deactivateGuide(
|
||||
guide: GuideState
|
||||
): Promise<{ pluginState: PluginState } | undefined> {
|
||||
return await this.updatePluginState(
|
||||
{
|
||||
...guide,
|
||||
isActive: false,
|
||||
status: 'quit',
|
||||
guide: {
|
||||
...guide,
|
||||
isActive: false,
|
||||
},
|
||||
},
|
||||
false
|
||||
);
|
||||
|
@ -204,27 +230,27 @@ export class ApiService implements GuidedOnboardingApi {
|
|||
* @param {GuideId} guideId the id of the guide (one of search, observability, security)
|
||||
* @return {Promise} a promise with the updated guide state
|
||||
*/
|
||||
public async completeGuide(guideId: GuideId): Promise<{ state: GuideState } | undefined> {
|
||||
const guideState = await firstValueFrom(this.fetchActiveGuideState$());
|
||||
public async completeGuide(guideId: GuideId): Promise<{ pluginState: PluginState } | undefined> {
|
||||
const pluginState = await firstValueFrom(this.fetchPluginState$());
|
||||
|
||||
// For now, returning undefined if consumer attempts to complete a guide that is not active
|
||||
if (guideState?.guideId !== guideId) {
|
||||
return undefined;
|
||||
}
|
||||
if (!isGuideActive(pluginState, guideId)) return undefined;
|
||||
|
||||
const { activeGuide } = pluginState!;
|
||||
|
||||
// All steps should be complete at this point
|
||||
// However, we do a final check here as a safeguard
|
||||
const allStepsComplete =
|
||||
Boolean(guideState.steps.find((step) => step.status !== 'complete')) === false;
|
||||
Boolean(activeGuide!.steps.find((step) => step.status !== 'complete')) === false;
|
||||
|
||||
if (allStepsComplete) {
|
||||
const updatedGuide: GuideState = {
|
||||
...guideState,
|
||||
...activeGuide!,
|
||||
isActive: false,
|
||||
status: 'complete',
|
||||
};
|
||||
|
||||
return await this.updateGuideState(updatedGuide, false);
|
||||
return await this.updatePluginState({ status: 'complete', guide: updatedGuide }, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -237,8 +263,11 @@ export class ApiService implements GuidedOnboardingApi {
|
|||
* @return {Observable} an observable with the boolean value
|
||||
*/
|
||||
public isGuideStepActive$(guideId: GuideId, stepId: GuideStepIds): Observable<boolean> {
|
||||
return this.fetchActiveGuideState$().pipe(
|
||||
map((activeGuideState) => isStepInProgress(activeGuideState, guideId, stepId))
|
||||
return this.fetchPluginState$().pipe(
|
||||
map((pluginState) => {
|
||||
if (!isGuideActive(pluginState, guideId)) return false;
|
||||
return isStepInProgress(pluginState!.activeGuide, guideId, stepId);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -252,15 +281,16 @@ export class ApiService implements GuidedOnboardingApi {
|
|||
public async startGuideStep(
|
||||
guideId: GuideId,
|
||||
stepId: GuideStepIds
|
||||
): Promise<{ state: GuideState } | undefined> {
|
||||
const guideState = await firstValueFrom(this.fetchActiveGuideState$());
|
||||
): Promise<{ pluginState: PluginState } | undefined> {
|
||||
const pluginState = await firstValueFrom(this.fetchPluginState$());
|
||||
|
||||
// For now, returning undefined if consumer attempts to start a step for a guide that isn't active
|
||||
if (guideState?.guideId !== guideId) {
|
||||
if (!isGuideActive(pluginState, guideId)) {
|
||||
return undefined;
|
||||
}
|
||||
const { activeGuide } = pluginState!;
|
||||
|
||||
const updatedSteps: GuideStep[] = guideState.steps.map((step) => {
|
||||
const updatedSteps: GuideStep[] = activeGuide!.steps.map((step) => {
|
||||
// Mark the current step as in_progress
|
||||
if (step.id === stepId) {
|
||||
return {
|
||||
|
@ -280,7 +310,7 @@ export class ApiService implements GuidedOnboardingApi {
|
|||
steps: updatedSteps,
|
||||
};
|
||||
|
||||
return await this.updateGuideState(currentGuide, false);
|
||||
return await this.updatePluginState({ guide: currentGuide }, false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -293,23 +323,22 @@ export class ApiService implements GuidedOnboardingApi {
|
|||
public async completeGuideStep(
|
||||
guideId: GuideId,
|
||||
stepId: GuideStepIds
|
||||
): Promise<{ state: GuideState } | undefined> {
|
||||
const guideState = await firstValueFrom(this.fetchActiveGuideState$());
|
||||
|
||||
): 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
|
||||
if (guideState?.guideId !== guideId) {
|
||||
if (!isGuideActive(pluginState, guideId)) {
|
||||
return undefined;
|
||||
}
|
||||
const { activeGuide } = pluginState!;
|
||||
const isCurrentStepInProgress = isStepInProgress(activeGuide, guideId, stepId);
|
||||
const isCurrentStepReadyToComplete = isStepReadyToComplete(activeGuide, guideId, stepId);
|
||||
|
||||
const isCurrentStepInProgress = isStepInProgress(guideState, guideId, stepId);
|
||||
const isCurrentStepReadyToComplete = isStepReadyToComplete(guideState, guideId, stepId);
|
||||
|
||||
const stepConfig = getStepConfig(guideState.guideId, stepId);
|
||||
const stepConfig = getStepConfig(activeGuide!.guideId, stepId);
|
||||
const isManualCompletion = stepConfig ? !!stepConfig.manualCompletion : false;
|
||||
|
||||
if (isCurrentStepInProgress || isCurrentStepReadyToComplete) {
|
||||
const updatedSteps = getUpdatedSteps(
|
||||
guideState,
|
||||
activeGuide!,
|
||||
stepId,
|
||||
// if current step is in progress and configured for manual completion,
|
||||
// set the status to ready_to_complete
|
||||
|
@ -319,12 +348,14 @@ export class ApiService implements GuidedOnboardingApi {
|
|||
const currentGuide: GuideState = {
|
||||
guideId,
|
||||
isActive: true,
|
||||
status: getGuideStatusOnStepCompletion(guideState, guideId, stepId),
|
||||
status: getGuideStatusOnStepCompletion(activeGuide, guideId, stepId),
|
||||
steps: updatedSteps,
|
||||
};
|
||||
|
||||
return await this.updateGuideState(
|
||||
currentGuide,
|
||||
return await this.updatePluginState(
|
||||
{
|
||||
guide: currentGuide,
|
||||
},
|
||||
// the panel is opened when the step is being set to complete.
|
||||
// that happens when the step is not configured for manual completion
|
||||
// or it's already ready_to_complete
|
||||
|
@ -343,29 +374,30 @@ export class ApiService implements GuidedOnboardingApi {
|
|||
* @return {Observable} an observable with the boolean value
|
||||
*/
|
||||
public isGuidedOnboardingActiveForIntegration$(integration?: string): Observable<boolean> {
|
||||
return this.fetchActiveGuideState$().pipe(
|
||||
map((state) => {
|
||||
return state ? isIntegrationInGuideStep(state, integration) : false;
|
||||
})
|
||||
return this.fetchPluginState$().pipe(
|
||||
map((state) => isIntegrationInGuideStep(state?.activeGuide, integration))
|
||||
);
|
||||
}
|
||||
|
||||
public async completeGuidedOnboardingForIntegration(
|
||||
integration?: string
|
||||
): Promise<{ state: GuideState } | undefined> {
|
||||
if (integration) {
|
||||
const currentState = await firstValueFrom(this.fetchActiveGuideState$());
|
||||
if (currentState) {
|
||||
const inProgressStepId = getInProgressStepId(currentState);
|
||||
if (inProgressStepId) {
|
||||
const isIntegrationStepActive = isIntegrationInGuideStep(currentState, integration);
|
||||
if (isIntegrationStepActive) {
|
||||
return await this.completeGuideStep(currentState?.guideId, inProgressStepId);
|
||||
}
|
||||
}
|
||||
}
|
||||
): Promise<{ pluginState: PluginState } | undefined> {
|
||||
if (!integration) return undefined;
|
||||
const pluginState = await firstValueFrom(this.fetchPluginState$());
|
||||
if (!isGuideActive(pluginState)) return undefined;
|
||||
const { activeGuide } = pluginState!;
|
||||
const inProgressStepId = getInProgressStepId(activeGuide!);
|
||||
if (!inProgressStepId) return undefined;
|
||||
const isIntegrationStepActive = isIntegrationInGuideStep(activeGuide!, integration);
|
||||
if (isIntegrationStepActive) {
|
||||
return await this.completeGuideStep(activeGuide!.guideId, inProgressStepId);
|
||||
}
|
||||
}
|
||||
|
||||
public async skipGuidedOnboarding(): Promise<{ pluginState: PluginState } | undefined> {
|
||||
// TODO error handling and loading state
|
||||
return await this.updatePluginState({ status: 'skipped' }, false);
|
||||
}
|
||||
}
|
||||
|
||||
export const apiService = new ApiService();
|
||||
|
|
|
@ -15,6 +15,7 @@ import type {
|
|||
} from '@kbn/guided-onboarding';
|
||||
import { guidesConfig } from '../constants/guides_config';
|
||||
import { GuideConfig, StepConfig } from '../types';
|
||||
import type { PluginState } from '../../common/types';
|
||||
|
||||
export const getGuideConfig = (guideId?: GuideId): GuideConfig | undefined => {
|
||||
if (guideId && Object.keys(guidesConfig).includes(guideId)) {
|
||||
|
@ -60,17 +61,28 @@ const getInProgressStepConfig = (state: GuideState): StepConfig | undefined => {
|
|||
}
|
||||
};
|
||||
|
||||
export const isIntegrationInGuideStep = (state: GuideState, integration?: string): boolean => {
|
||||
if (state.isActive) {
|
||||
const stepConfig = getInProgressStepConfig(state);
|
||||
return stepConfig ? stepConfig.integration === integration : false;
|
||||
}
|
||||
return false;
|
||||
export const isIntegrationInGuideStep = (
|
||||
guideState?: GuideState,
|
||||
integration?: string
|
||||
): boolean => {
|
||||
if (!guideState || !guideState.isActive) return false;
|
||||
|
||||
const stepConfig = getInProgressStepConfig(guideState);
|
||||
return stepConfig ? stepConfig.integration === integration : false;
|
||||
};
|
||||
|
||||
const isGuideActive = (guideState: GuideState | undefined, guideId: GuideId): boolean => {
|
||||
// false if guideState is undefined or the guide is not active
|
||||
return !!(guideState && guideState.isActive && guideState.guideId === guideId);
|
||||
export const isGuideActive = (pluginState?: PluginState, guideId?: GuideId): boolean => {
|
||||
// false if pluginState is undefined or plugin state is not in progress
|
||||
// or active guide is undefined
|
||||
if (!pluginState || pluginState.status !== 'in_progress' || !pluginState.activeGuide) {
|
||||
return false;
|
||||
}
|
||||
// guideId is passed, check that it's the id of the active guide
|
||||
if (guideId) {
|
||||
const { activeGuide } = pluginState;
|
||||
return !!(activeGuide.isActive && activeGuide.guideId === guideId);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const isStepInProgress = (
|
||||
|
@ -78,12 +90,10 @@ export const isStepInProgress = (
|
|||
guideId: GuideId,
|
||||
stepId: GuideStepIds
|
||||
): boolean => {
|
||||
if (!isGuideActive(guideState, guideId)) {
|
||||
return false;
|
||||
}
|
||||
if (!guideState || !guideState.isActive) return false;
|
||||
|
||||
// false if the step is not 'in_progress'
|
||||
const selectedStep = guideState!.steps.find((step) => step.id === stepId);
|
||||
const selectedStep = guideState.steps.find((step) => step.id === stepId);
|
||||
return selectedStep ? selectedStep.status === 'in_progress' : false;
|
||||
};
|
||||
|
||||
|
@ -92,10 +102,7 @@ export const isStepReadyToComplete = (
|
|||
guideId: GuideId,
|
||||
stepId: GuideStepIds
|
||||
): boolean => {
|
||||
if (!isGuideActive(guideState, guideId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!guideState || !guideState.isActive) return false;
|
||||
// false if the step is not 'ready_to_complete'
|
||||
const selectedStep = guideState!.steps.find((step) => step.id === stepId);
|
||||
return selectedStep ? selectedStep.status === 'ready_to_complete' : false;
|
||||
|
@ -136,7 +143,7 @@ export const getUpdatedSteps = (
|
|||
};
|
||||
|
||||
export const getGuideStatusOnStepCompletion = (
|
||||
guideState: GuideState,
|
||||
guideState: GuideState | undefined,
|
||||
guideId: GuideId,
|
||||
stepId: GuideStepIds
|
||||
): GuideStatus => {
|
||||
|
|
|
@ -11,6 +11,7 @@ import { Observable } from 'rxjs';
|
|||
import { HttpSetup } from '@kbn/core/public';
|
||||
import type { GuideState, GuideId, GuideStepIds, StepStatus } from '@kbn/guided-onboarding';
|
||||
import type { CloudStart } from '@kbn/cloud-plugin/public';
|
||||
import type { PluginStatus, PluginState } from '../common/types';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface GuidedOnboardingPluginSetup {}
|
||||
|
@ -24,31 +25,33 @@ export interface AppPluginStartDependencies {
|
|||
}
|
||||
|
||||
export interface GuidedOnboardingApi {
|
||||
setup: (httpClient: HttpSetup) => void;
|
||||
fetchActiveGuideState$: () => Observable<GuideState | undefined>;
|
||||
setup: (httpClient: HttpSetup, isCloudEnabled: boolean) => void;
|
||||
fetchPluginState$: () => Observable<PluginState | undefined>;
|
||||
fetchAllGuidesState: () => Promise<{ state: GuideState[] } | undefined>;
|
||||
updateGuideState: (
|
||||
newState: GuideState,
|
||||
updatePluginState: (
|
||||
state: { status?: PluginStatus; guide?: GuideState },
|
||||
panelState: boolean
|
||||
) => Promise<{ state: GuideState } | undefined>;
|
||||
) => Promise<{ pluginState: PluginState } | undefined>;
|
||||
activateGuide: (
|
||||
guideId: GuideId,
|
||||
guide?: GuideState
|
||||
) => Promise<{ state: GuideState } | undefined>;
|
||||
completeGuide: (guideId: GuideId) => Promise<{ state: GuideState } | undefined>;
|
||||
) => Promise<{ pluginState: PluginState } | undefined>;
|
||||
deactivateGuide: (guide: GuideState) => Promise<{ pluginState: PluginState } | undefined>;
|
||||
completeGuide: (guideId: GuideId) => Promise<{ pluginState: PluginState } | undefined>;
|
||||
isGuideStepActive$: (guideId: GuideId, stepId: GuideStepIds) => Observable<boolean>;
|
||||
startGuideStep: (
|
||||
guideId: GuideId,
|
||||
stepId: GuideStepIds
|
||||
) => Promise<{ state: GuideState } | undefined>;
|
||||
) => Promise<{ pluginState: PluginState } | undefined>;
|
||||
completeGuideStep: (
|
||||
guideId: GuideId,
|
||||
stepId: GuideStepIds
|
||||
) => Promise<{ state: GuideState } | undefined>;
|
||||
) => Promise<{ pluginState: PluginState } | undefined>;
|
||||
isGuidedOnboardingActiveForIntegration$: (integration?: string) => Observable<boolean>;
|
||||
completeGuidedOnboardingForIntegration: (
|
||||
integration?: string
|
||||
) => Promise<{ state: GuideState } | undefined>;
|
||||
) => Promise<{ pluginState: PluginState } | undefined>;
|
||||
skipGuidedOnboarding: () => Promise<{ pluginState: PluginState } | undefined>;
|
||||
isGuidePanelOpen$: Observable<boolean>;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* 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 { SavedObjectsClient } from '@kbn/core/server';
|
||||
import { GuideState } from '@kbn/guided-onboarding';
|
||||
import { guideStateSavedObjectsType } from '../saved_objects';
|
||||
|
||||
export const findGuideById = async (savedObjectsClient: SavedObjectsClient, guideId: string) => {
|
||||
return savedObjectsClient.find<GuideState>({
|
||||
type: guideStateSavedObjectsType,
|
||||
search: `"${guideId}"`,
|
||||
searchFields: ['guideId'],
|
||||
});
|
||||
};
|
||||
|
||||
export const findActiveGuide = async (savedObjectsClient: SavedObjectsClient) => {
|
||||
return savedObjectsClient.find<GuideState>({
|
||||
type: guideStateSavedObjectsType,
|
||||
search: 'true',
|
||||
searchFields: ['isActive'],
|
||||
});
|
||||
};
|
||||
|
||||
export const findAllGuides = async (savedObjectsClient: SavedObjectsClient) => {
|
||||
return savedObjectsClient.find<GuideState>({ type: guideStateSavedObjectsType });
|
||||
};
|
||||
|
||||
export const updateGuideState = async (
|
||||
savedObjectsClient: SavedObjectsClient,
|
||||
updatedGuideState: GuideState
|
||||
) => {
|
||||
const selectedGuideSO = await findGuideById(savedObjectsClient, updatedGuideState.guideId);
|
||||
|
||||
// If the SO already exists, update it, else create a new SO
|
||||
if (selectedGuideSO.total > 0) {
|
||||
const updatedGuides = [];
|
||||
const selectedGuide = selectedGuideSO.saved_objects[0];
|
||||
|
||||
updatedGuides.push({
|
||||
type: guideStateSavedObjectsType,
|
||||
id: selectedGuide.id,
|
||||
attributes: {
|
||||
...updatedGuideState,
|
||||
},
|
||||
});
|
||||
|
||||
// If we are activating a new guide, we need to check if there is a different, existing active guide
|
||||
// If yes, we need to mark it as inactive (only 1 guide can be active at a time)
|
||||
if (updatedGuideState.isActive) {
|
||||
const activeGuideSO = await findActiveGuide(savedObjectsClient);
|
||||
|
||||
if (activeGuideSO.total > 0) {
|
||||
const activeGuide = activeGuideSO.saved_objects[0];
|
||||
if (activeGuide.attributes.guideId !== updatedGuideState.guideId) {
|
||||
updatedGuides.push({
|
||||
type: guideStateSavedObjectsType,
|
||||
id: activeGuide.id,
|
||||
attributes: {
|
||||
...activeGuide.attributes,
|
||||
isActive: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updatedGuidesResponse = await savedObjectsClient.bulkUpdate(updatedGuides);
|
||||
|
||||
return updatedGuidesResponse;
|
||||
} else {
|
||||
// If we are activating a new guide, we need to check if there is an existing active guide
|
||||
// If yes, we need to mark it as inactive (only 1 guide can be active at a time)
|
||||
if (updatedGuideState.isActive) {
|
||||
const activeGuideSO = await findActiveGuide(savedObjectsClient);
|
||||
|
||||
if (activeGuideSO.total > 0) {
|
||||
const activeGuide = activeGuideSO.saved_objects[0];
|
||||
await savedObjectsClient.update(guideStateSavedObjectsType, activeGuide.id, {
|
||||
...activeGuide.attributes,
|
||||
isActive: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const createdGuideResponse = await savedObjectsClient.create(
|
||||
guideStateSavedObjectsType,
|
||||
updatedGuideState,
|
||||
{
|
||||
id: updatedGuideState.guideId,
|
||||
}
|
||||
);
|
||||
|
||||
return createdGuideResponse;
|
||||
}
|
||||
};
|
15
src/plugins/guided_onboarding/server/helpers/index.ts
Normal file
15
src/plugins/guided_onboarding/server/helpers/index.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 {
|
||||
findActiveGuide,
|
||||
findAllGuides,
|
||||
findGuideById,
|
||||
updateGuideState,
|
||||
} from './guide_state_utils';
|
||||
export { updatePluginStatus, calculateIsActivePeriod, getPluginState } from './plugin_state_utils';
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 { calculateIsActivePeriod } from './plugin_state_utils';
|
||||
|
||||
describe('calculateIsActivePeriod', () => {
|
||||
let result: boolean;
|
||||
it('returns false if creationDate is undefined', () => {
|
||||
result = calculateIsActivePeriod(undefined);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false if after the active period (35d from creation date)', () => {
|
||||
// currently active period is 30 days long after the creation date
|
||||
const duration35DaysInMilliseconds = 35 * 24 * 60 * 60 * 1000;
|
||||
const now = new Date();
|
||||
const creationDate35DaysAgo = new Date(now.getTime() - duration35DaysInMilliseconds);
|
||||
result = calculateIsActivePeriod(creationDate35DaysAgo.toISOString());
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true if in the active period (15d from creation date)', () => {
|
||||
// currently active period is 30 days long after the creation date
|
||||
const duration15DaysInMilliseconds = 15 * 24 * 60 * 60 * 1000;
|
||||
const now = new Date();
|
||||
const creationDate15DaysAgo = new Date(now.getTime() - duration15DaysInMilliseconds);
|
||||
result = calculateIsActivePeriod(creationDate15DaysAgo.toISOString());
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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 { SavedObjectsClient } from '@kbn/core/server';
|
||||
import { findActiveGuide } from './guide_state_utils';
|
||||
import { PluginState, PluginStatus } from '../../common/types';
|
||||
import {
|
||||
pluginStateSavedObjectsId,
|
||||
pluginStateSavedObjectsType,
|
||||
PluginStateSO,
|
||||
} from '../saved_objects';
|
||||
|
||||
// hard code the duration to 30 days for now https://github.com/elastic/kibana/issues/144997
|
||||
const activePeriodDurationInMilliseconds = 30 * 24 * 60 * 60 * 1000;
|
||||
export const calculateIsActivePeriod = (creationDate?: string): boolean => {
|
||||
if (!creationDate) return false;
|
||||
const parsedCreationDate = Date.parse(creationDate);
|
||||
const endOfActivePeriodDate = new Date(parsedCreationDate + activePeriodDurationInMilliseconds);
|
||||
const now = new Date();
|
||||
return now < endOfActivePeriodDate;
|
||||
};
|
||||
|
||||
export const getPluginState = async (savedObjectsClient: SavedObjectsClient) => {
|
||||
const pluginStateSO = await savedObjectsClient.find<PluginStateSO>({
|
||||
type: pluginStateSavedObjectsType,
|
||||
});
|
||||
if (pluginStateSO.saved_objects.length === 1) {
|
||||
const { status, creationDate } = pluginStateSO.saved_objects[0].attributes;
|
||||
const isActivePeriod = calculateIsActivePeriod(creationDate);
|
||||
const activeGuideSO = await findActiveGuide(savedObjectsClient);
|
||||
const pluginState: PluginState = { status: status as PluginStatus, isActivePeriod };
|
||||
if (activeGuideSO.saved_objects.length === 1) {
|
||||
pluginState.activeGuide = activeGuideSO.saved_objects[0].attributes;
|
||||
}
|
||||
return pluginState;
|
||||
} else {
|
||||
// create a SO to keep track of the correct creation date
|
||||
await updatePluginStatus(savedObjectsClient, 'not_started');
|
||||
return {
|
||||
status: 'not_started',
|
||||
isActivePeriod: true,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const updatePluginStatus = async (
|
||||
savedObjectsClient: SavedObjectsClient,
|
||||
status: string
|
||||
) => {
|
||||
return await savedObjectsClient.update<PluginStateSO>(
|
||||
pluginStateSavedObjectsType,
|
||||
pluginStateSavedObjectsId,
|
||||
{
|
||||
status,
|
||||
},
|
||||
{
|
||||
// if there is no saved object yet, insert a new SO with the creation date
|
||||
upsert: { status, creationDate: new Date().toISOString() },
|
||||
}
|
||||
);
|
||||
};
|
|
@ -10,7 +10,7 @@ import { PluginInitializerContext, CoreSetup, Plugin, Logger } from '@kbn/core/s
|
|||
|
||||
import { GuidedOnboardingPluginSetup, GuidedOnboardingPluginStart } from './types';
|
||||
import { defineRoutes } from './routes';
|
||||
import { guidedSetupSavedObjects } from './saved_objects';
|
||||
import { guideStateSavedObjects, pluginStateSavedObjects } from './saved_objects';
|
||||
|
||||
export class GuidedOnboardingPlugin
|
||||
implements Plugin<GuidedOnboardingPluginSetup, GuidedOnboardingPluginStart>
|
||||
|
@ -29,7 +29,8 @@ export class GuidedOnboardingPlugin
|
|||
defineRoutes(router);
|
||||
|
||||
// register saved objects
|
||||
core.savedObjects.registerType(guidedSetupSavedObjects);
|
||||
core.savedObjects.registerType(guideStateSavedObjects);
|
||||
core.savedObjects.registerType(pluginStateSavedObjects);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 { IRouter, SavedObjectsClient } from '@kbn/core/server';
|
||||
import { API_BASE_PATH } from '../../common/constants';
|
||||
import { findAllGuides } from '../helpers';
|
||||
|
||||
export const registerGetGuideStateRoute = (router: IRouter) => {
|
||||
// Fetch all guides state
|
||||
router.get(
|
||||
{
|
||||
path: `${API_BASE_PATH}/guides`,
|
||||
validate: false,
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const coreContext = await context.core;
|
||||
const soClient = coreContext.savedObjects.client as SavedObjectsClient;
|
||||
|
||||
const existingGuides = await findAllGuides(soClient);
|
||||
|
||||
if (existingGuides.total > 0) {
|
||||
const guidesState = existingGuides.saved_objects.map((guide) => guide.attributes);
|
||||
return response.ok({
|
||||
body: { state: guidesState },
|
||||
});
|
||||
} else {
|
||||
// If no SO exists, we assume state hasn't been stored yet and return an empty array
|
||||
return response.ok({
|
||||
body: { state: [] },
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -6,159 +6,13 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import type { IRouter, SavedObjectsClient } from '@kbn/core/server';
|
||||
import type { GuideState } from '@kbn/guided-onboarding';
|
||||
import { API_BASE_PATH } from '../../common/constants';
|
||||
import { guidedSetupSavedObjectsType } from '../saved_objects';
|
||||
|
||||
const findGuideById = async (savedObjectsClient: SavedObjectsClient, guideId: string) => {
|
||||
return savedObjectsClient.find<GuideState>({
|
||||
type: guidedSetupSavedObjectsType,
|
||||
search: `"${guideId}"`,
|
||||
searchFields: ['guideId'],
|
||||
});
|
||||
};
|
||||
|
||||
const findActiveGuide = async (savedObjectsClient: SavedObjectsClient) => {
|
||||
return savedObjectsClient.find<GuideState>({
|
||||
type: guidedSetupSavedObjectsType,
|
||||
search: 'true',
|
||||
searchFields: ['isActive'],
|
||||
});
|
||||
};
|
||||
|
||||
const findAllGuides = async (savedObjectsClient: SavedObjectsClient) => {
|
||||
return savedObjectsClient.find<GuideState>({ type: guidedSetupSavedObjectsType });
|
||||
};
|
||||
import type { IRouter } from '@kbn/core/server';
|
||||
import { registerGetGuideStateRoute } from './guide_state_routes';
|
||||
import { registerGetPluginStateRoute, registerPutPluginStateRoute } from './plugin_state_routes';
|
||||
|
||||
export function defineRoutes(router: IRouter) {
|
||||
// Fetch all guides state; optionally pass the query param ?active=true to only return the active guide
|
||||
router.get(
|
||||
{
|
||||
path: `${API_BASE_PATH}/state`,
|
||||
validate: {
|
||||
query: schema.object({
|
||||
active: schema.maybe(schema.boolean()),
|
||||
}),
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const coreContext = await context.core;
|
||||
const soClient = coreContext.savedObjects.client as SavedObjectsClient;
|
||||
registerGetGuideStateRoute(router);
|
||||
|
||||
const existingGuides =
|
||||
request.query.active === true
|
||||
? await findActiveGuide(soClient)
|
||||
: await findAllGuides(soClient);
|
||||
|
||||
if (existingGuides.total > 0) {
|
||||
const guidesState = existingGuides.saved_objects.map((guide) => guide.attributes);
|
||||
return response.ok({
|
||||
body: { state: guidesState },
|
||||
});
|
||||
} else {
|
||||
// If no SO exists, we assume state hasn't been stored yet and return an empty array
|
||||
return response.ok({
|
||||
body: { state: [] },
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Update the guide state for the passed guideId;
|
||||
// will also check any existing active guides and update them to an "inactive" state
|
||||
router.put(
|
||||
{
|
||||
path: `${API_BASE_PATH}/state`,
|
||||
validate: {
|
||||
body: schema.object({
|
||||
status: schema.string(),
|
||||
guideId: schema.string(),
|
||||
isActive: schema.boolean(),
|
||||
steps: schema.arrayOf(
|
||||
schema.object({
|
||||
status: schema.string(),
|
||||
id: schema.string(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const updatedGuideState = request.body;
|
||||
|
||||
const coreContext = await context.core;
|
||||
const savedObjectsClient = coreContext.savedObjects.client as SavedObjectsClient;
|
||||
|
||||
const selectedGuideSO = await findGuideById(savedObjectsClient, updatedGuideState.guideId);
|
||||
|
||||
// If the SO already exists, update it, else create a new SO
|
||||
if (selectedGuideSO.total > 0) {
|
||||
const updatedGuides = [];
|
||||
const selectedGuide = selectedGuideSO.saved_objects[0];
|
||||
|
||||
updatedGuides.push({
|
||||
type: guidedSetupSavedObjectsType,
|
||||
id: selectedGuide.id,
|
||||
attributes: {
|
||||
...updatedGuideState,
|
||||
},
|
||||
});
|
||||
|
||||
// If we are activating a new guide, we need to check if there is a different, existing active guide
|
||||
// If yes, we need to mark it as inactive (only 1 guide can be active at a time)
|
||||
if (updatedGuideState.isActive) {
|
||||
const activeGuideSO = await findActiveGuide(savedObjectsClient);
|
||||
|
||||
if (activeGuideSO.total > 0) {
|
||||
const activeGuide = activeGuideSO.saved_objects[0];
|
||||
if (activeGuide.attributes.guideId !== updatedGuideState.guideId) {
|
||||
updatedGuides.push({
|
||||
type: guidedSetupSavedObjectsType,
|
||||
id: activeGuide.id,
|
||||
attributes: {
|
||||
...activeGuide.attributes,
|
||||
isActive: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updatedGuidesResponse = await savedObjectsClient.bulkUpdate(updatedGuides);
|
||||
|
||||
return response.ok({
|
||||
body: {
|
||||
state: updatedGuidesResponse,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// If we are activating a new guide, we need to check if there is an existing active guide
|
||||
// If yes, we need to mark it as inactive (only 1 guide can be active at a time)
|
||||
if (updatedGuideState.isActive) {
|
||||
const activeGuideSO = await findActiveGuide(savedObjectsClient);
|
||||
|
||||
if (activeGuideSO.total > 0) {
|
||||
const activeGuide = activeGuideSO.saved_objects[0];
|
||||
await savedObjectsClient.update(guidedSetupSavedObjectsType, activeGuide.id, {
|
||||
...activeGuide.attributes,
|
||||
isActive: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const createdGuideResponse = await savedObjectsClient.create(
|
||||
guidedSetupSavedObjectsType,
|
||||
updatedGuideState
|
||||
);
|
||||
|
||||
return response.ok({
|
||||
body: {
|
||||
state: createdGuideResponse,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
registerGetPluginStateRoute(router);
|
||||
registerPutPluginStateRoute(router);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* 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 { IRouter, SavedObjectsClient } from '@kbn/core/server';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { GuideState } from '@kbn/guided-onboarding';
|
||||
import { getPluginState, updatePluginStatus } from '../helpers/plugin_state_utils';
|
||||
import { API_BASE_PATH } from '../../common/constants';
|
||||
import { updateGuideState } from '../helpers';
|
||||
|
||||
export const registerGetPluginStateRoute = (router: IRouter) => {
|
||||
router.get(
|
||||
{
|
||||
path: `${API_BASE_PATH}/state`,
|
||||
validate: false,
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const coreContext = await context.core;
|
||||
const savedObjectsClient = coreContext.savedObjects.client as SavedObjectsClient;
|
||||
const pluginState = await getPluginState(savedObjectsClient);
|
||||
return response.ok({
|
||||
body: {
|
||||
pluginState,
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const registerPutPluginStateRoute = (router: IRouter) => {
|
||||
router.put(
|
||||
{
|
||||
path: `${API_BASE_PATH}/state`,
|
||||
validate: {
|
||||
body: schema.object({
|
||||
status: schema.maybe(schema.string()),
|
||||
guide: schema.maybe(
|
||||
schema.object({
|
||||
status: schema.string(),
|
||||
guideId: schema.string(),
|
||||
isActive: schema.boolean(),
|
||||
steps: schema.arrayOf(
|
||||
schema.object({
|
||||
status: schema.string(),
|
||||
id: schema.string(),
|
||||
})
|
||||
),
|
||||
})
|
||||
),
|
||||
}),
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const { status, guide } = request.body as { status?: string; guide?: GuideState };
|
||||
|
||||
const coreContext = await context.core;
|
||||
const savedObjectsClient = coreContext.savedObjects.client as SavedObjectsClient;
|
||||
|
||||
if (status) {
|
||||
await updatePluginStatus(savedObjectsClient, status);
|
||||
}
|
||||
if (guide) {
|
||||
await updateGuideState(savedObjectsClient, guide);
|
||||
}
|
||||
|
||||
const pluginState = await getPluginState(savedObjectsClient);
|
||||
return response.ok({
|
||||
body: {
|
||||
pluginState,
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
|
@ -8,12 +8,12 @@
|
|||
|
||||
import { SavedObjectsType } from '@kbn/core/server';
|
||||
|
||||
export const guidedSetupSavedObjectsType = 'guided-onboarding-guide-state';
|
||||
export const guideStateSavedObjectsType = 'guided-onboarding-guide-state';
|
||||
|
||||
export const guidedSetupSavedObjects: SavedObjectsType = {
|
||||
name: guidedSetupSavedObjectsType,
|
||||
export const guideStateSavedObjects: SavedObjectsType = {
|
||||
name: guideStateSavedObjectsType,
|
||||
hidden: false,
|
||||
// make it available in all spaces for now
|
||||
// make it available in all spaces for now https://github.com/elastic/kibana/issues/144227
|
||||
namespaceType: 'agnostic',
|
||||
mappings: {
|
||||
dynamic: false,
|
||||
|
@ -27,3 +27,24 @@ export const guidedSetupSavedObjects: SavedObjectsType = {
|
|||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const pluginStateSavedObjectsType = 'guided-onboarding-plugin-state';
|
||||
export const pluginStateSavedObjectsId = 'guided-onboarding-plugin-state-id';
|
||||
|
||||
export const pluginStateSavedObjects: SavedObjectsType = {
|
||||
name: pluginStateSavedObjectsType,
|
||||
hidden: false,
|
||||
// make it available in all spaces for now https://github.com/elastic/kibana/issues/144227
|
||||
namespaceType: 'agnostic',
|
||||
mappings: {
|
||||
dynamic: false,
|
||||
// we don't query this SO so no need for mapping properties, see PluginState intefrace
|
||||
properties: {},
|
||||
},
|
||||
};
|
||||
|
||||
// plugin state SO interface
|
||||
export interface PluginStateSO {
|
||||
status: string;
|
||||
creationDate: string;
|
||||
}
|
||||
|
|
|
@ -6,4 +6,11 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { guidedSetupSavedObjects, guidedSetupSavedObjectsType } from './guided_setup';
|
||||
export {
|
||||
guideStateSavedObjects,
|
||||
guideStateSavedObjectsType,
|
||||
pluginStateSavedObjects,
|
||||
pluginStateSavedObjectsId,
|
||||
pluginStateSavedObjectsType,
|
||||
} from './guided_setup';
|
||||
export type { PluginStateSO } from './guided_setup';
|
||||
|
|
|
@ -12,6 +12,7 @@ import { findTestSubject, mountWithIntl } from '@kbn/test-jest-helpers';
|
|||
|
||||
import { GettingStarted } from './getting_started';
|
||||
import { KEY_ENABLE_WELCOME } from '../home';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
jest.mock('../../kibana_services', () => {
|
||||
const { chromeServiceMock, applicationServiceMock } =
|
||||
|
@ -35,6 +36,7 @@ jest.mock('../../kibana_services', () => {
|
|||
},
|
||||
guidedOnboardingService: {
|
||||
fetchAllGuidesState: jest.fn(),
|
||||
skipGuidedOnboarding: jest.fn(),
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
@ -59,7 +61,12 @@ describe('getting started', () => {
|
|||
test('skip button should disable home welcome screen', async () => {
|
||||
const component = mountWithIntl(<GettingStarted />);
|
||||
const skipButton = findTestSubject(component, 'onboarding--skipGuideLink');
|
||||
skipButton.simulate('click');
|
||||
|
||||
await act(async () => {
|
||||
await skipButton.simulate('click');
|
||||
});
|
||||
|
||||
component.update();
|
||||
|
||||
expect(localStorage.getItem(KEY_ENABLE_WELCOME)).toBe('false');
|
||||
});
|
||||
|
|
|
@ -84,7 +84,8 @@ export const GettingStarted = () => {
|
|||
}
|
||||
}, [cloud, history]);
|
||||
|
||||
const onSkip = () => {
|
||||
const onSkip = async () => {
|
||||
await guidedOnboardingService?.skipGuidedOnboarding();
|
||||
trackUiMetric(METRIC_TYPE.CLICK, 'guided_onboarding__skipped');
|
||||
// disable welcome screen on the home page
|
||||
localStorage.setItem(KEY_ENABLE_WELCOME, JSON.stringify(false));
|
||||
|
|
50
test/api_integration/apis/guided_onboarding/get_guides.ts
Normal file
50
test/api_integration/apis/guided_onboarding/get_guides.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { testGuideStep1ActiveState } from '@kbn/guided-onboarding-plugin/public/services/api.mocks';
|
||||
import {
|
||||
guideStateSavedObjectsType,
|
||||
pluginStateSavedObjectsType,
|
||||
} from '@kbn/guided-onboarding-plugin/server/saved_objects/guided_setup';
|
||||
import type { FtrProviderContext } from '../../ftr_provider_context';
|
||||
import { createGuides } from './helpers';
|
||||
|
||||
const getGuidesPath = '/api/guided_onboarding/guides';
|
||||
export default function testGetGuidesState({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
|
||||
describe('GET /api/guided_onboarding/guides', () => {
|
||||
afterEach(async () => {
|
||||
// Clean up saved objects
|
||||
await kibanaServer.savedObjects.clean({
|
||||
types: [guideStateSavedObjectsType, pluginStateSavedObjectsType],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an empty array if no guides', async () => {
|
||||
const response = await supertest.get(getGuidesPath).expect(200);
|
||||
expect(response.body).not.to.be.empty();
|
||||
expect(response.body.state).to.be.empty();
|
||||
});
|
||||
|
||||
it('returns all created guides (active and inactive)', async () => {
|
||||
await createGuides(kibanaServer, [
|
||||
testGuideStep1ActiveState,
|
||||
{ ...testGuideStep1ActiveState, guideId: 'search' },
|
||||
]);
|
||||
const response = await supertest.get(getGuidesPath).expect(200);
|
||||
expect(response.body).not.to.be.empty();
|
||||
expect(response.body.state).to.eql([
|
||||
testGuideStep1ActiveState,
|
||||
{ ...testGuideStep1ActiveState, guideId: 'search' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
114
test/api_integration/apis/guided_onboarding/get_state.ts
Normal file
114
test/api_integration/apis/guided_onboarding/get_state.ts
Normal file
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import {
|
||||
testGuideStep1ActiveState,
|
||||
testGuideNotActiveState,
|
||||
mockPluginStateNotStarted,
|
||||
} from '@kbn/guided-onboarding-plugin/public/services/api.mocks';
|
||||
import {
|
||||
guideStateSavedObjectsType,
|
||||
pluginStateSavedObjectsType,
|
||||
} from '@kbn/guided-onboarding-plugin/server/saved_objects/guided_setup';
|
||||
import type { FtrProviderContext } from '../../ftr_provider_context';
|
||||
import { createPluginState, createGuides } from './helpers';
|
||||
|
||||
const getDateXDaysAgo = (daysAgo: number): string => {
|
||||
const date = new Date();
|
||||
date.setDate(new Date().getDate() - daysAgo);
|
||||
return date.toISOString();
|
||||
};
|
||||
|
||||
const getStatePath = '/api/guided_onboarding/state';
|
||||
export default function testGetState({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
|
||||
describe('GET /api/guided_onboarding/state', () => {
|
||||
afterEach(async () => {
|
||||
// Clean up saved objects
|
||||
await kibanaServer.savedObjects.clean({
|
||||
types: [guideStateSavedObjectsType, pluginStateSavedObjectsType],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the default plugin state if no saved objects', async () => {
|
||||
const response = await supertest.get(getStatePath).expect(200);
|
||||
expect(response.body.pluginState).not.to.be.empty();
|
||||
expect(response.body).to.eql({
|
||||
pluginState: mockPluginStateNotStarted,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the plugin state with an active guide', async () => {
|
||||
// Create an active guide
|
||||
await createGuides(kibanaServer, [testGuideStep1ActiveState]);
|
||||
|
||||
// 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).not.to.be.empty();
|
||||
expect(response.body).to.eql({
|
||||
pluginState: {
|
||||
status: 'in_progress',
|
||||
isActivePeriod: true,
|
||||
activeGuide: testGuideStep1ActiveState,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns only the plugin state when no guide is active', async () => {
|
||||
// Create an active guide
|
||||
await createGuides(kibanaServer, [testGuideNotActiveState]);
|
||||
|
||||
// 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).not.to.be.empty();
|
||||
expect(response.body).to.eql({
|
||||
pluginState: {
|
||||
status: 'in_progress',
|
||||
isActivePeriod: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns isActivePeriod=false if creationDate is 40 days ago', async () => {
|
||||
// Create a plugin state
|
||||
await createPluginState(kibanaServer, {
|
||||
status: 'not_started',
|
||||
creationDate: getDateXDaysAgo(40),
|
||||
});
|
||||
|
||||
const response = await supertest.get(getStatePath).expect(200);
|
||||
expect(response.body.pluginState).not.to.be.empty();
|
||||
expect(response.body.pluginState.isActivePeriod).to.eql(false);
|
||||
});
|
||||
|
||||
it('returns isActivePeriod=true if creationDate is 20 days ago', async () => {
|
||||
// Create a plugin state
|
||||
await createPluginState(kibanaServer, {
|
||||
status: 'not_started',
|
||||
creationDate: getDateXDaysAgo(20),
|
||||
});
|
||||
|
||||
const response = await supertest.get(getStatePath).expect(200);
|
||||
expect(response.body.pluginState).not.to.be.empty();
|
||||
expect(response.body.pluginState.isActivePeriod).to.eql(true);
|
||||
});
|
||||
});
|
||||
}
|
36
test/api_integration/apis/guided_onboarding/helpers.ts
Normal file
36
test/api_integration/apis/guided_onboarding/helpers.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 { KbnClient } from '@kbn/test';
|
||||
import {
|
||||
guideStateSavedObjectsType,
|
||||
pluginStateSavedObjectsId,
|
||||
pluginStateSavedObjectsType,
|
||||
PluginStateSO,
|
||||
} from '@kbn/guided-onboarding-plugin/server/saved_objects';
|
||||
import { GuideState } from '@kbn/guided-onboarding';
|
||||
|
||||
export const createPluginState = async (client: KbnClient, state: PluginStateSO) => {
|
||||
await client.savedObjects.create({
|
||||
type: pluginStateSavedObjectsType,
|
||||
id: pluginStateSavedObjectsId,
|
||||
overwrite: true,
|
||||
attributes: state,
|
||||
});
|
||||
};
|
||||
|
||||
export const createGuides = async (client: KbnClient, guides: GuideState[]) => {
|
||||
for (const guide of guides) {
|
||||
await client.savedObjects.create({
|
||||
type: guideStateSavedObjectsType,
|
||||
id: guide.guideId,
|
||||
overwrite: true,
|
||||
attributes: guide,
|
||||
});
|
||||
}
|
||||
};
|
|
@ -1,8 +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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
* 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 { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
@ -11,5 +12,6 @@ export default function apiIntegrationTests({ loadTestFile }: FtrProviderContext
|
|||
describe('guided onboarding', () => {
|
||||
loadTestFile(require.resolve('./get_state'));
|
||||
loadTestFile(require.resolve('./put_state'));
|
||||
loadTestFile(require.resolve('./get_guides'));
|
||||
});
|
||||
}
|
164
test/api_integration/apis/guided_onboarding/put_state.ts
Normal file
164
test/api_integration/apis/guided_onboarding/put_state.ts
Normal file
|
@ -0,0 +1,164 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import {
|
||||
testGuideStep1ActiveState,
|
||||
testGuideNotActiveState,
|
||||
testGuide,
|
||||
} from '@kbn/guided-onboarding-plugin/public/services/api.mocks';
|
||||
import {
|
||||
pluginStateSavedObjectsType,
|
||||
pluginStateSavedObjectsId,
|
||||
guideStateSavedObjectsType,
|
||||
} from '@kbn/guided-onboarding-plugin/server/saved_objects/guided_setup';
|
||||
import type { FtrProviderContext } from '../../ftr_provider_context';
|
||||
import { createGuides, createPluginState } from './helpers';
|
||||
|
||||
const putStatePath = `/api/guided_onboarding/state`;
|
||||
export default function testPutState({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
|
||||
describe('PUT /api/guided_onboarding/state', () => {
|
||||
afterEach(async () => {
|
||||
// Clean up saved objects
|
||||
await kibanaServer.savedObjects.clean({
|
||||
types: [pluginStateSavedObjectsType, guideStateSavedObjectsType],
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a plugin saved object when updating the status and there is no state yet', async () => {
|
||||
const response = await supertest
|
||||
.put(putStatePath)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
status: 'in_progress',
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).to.eql({
|
||||
pluginState: {
|
||||
status: 'in_progress',
|
||||
isActivePeriod: true,
|
||||
},
|
||||
});
|
||||
|
||||
const createdSO = await kibanaServer.savedObjects.get({
|
||||
type: pluginStateSavedObjectsType,
|
||||
id: pluginStateSavedObjectsId,
|
||||
});
|
||||
|
||||
expect(createdSO.attributes.status).to.eql('in_progress');
|
||||
});
|
||||
|
||||
it('updates the plugin saved object when updating the status and there is already state', async () => {
|
||||
await createPluginState(kibanaServer, {
|
||||
status: 'not_started',
|
||||
creationDate: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const response = await supertest
|
||||
.put(putStatePath)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
status: 'in_progress',
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).to.eql({
|
||||
pluginState: {
|
||||
status: 'in_progress',
|
||||
isActivePeriod: true,
|
||||
},
|
||||
});
|
||||
|
||||
const createdSO = await kibanaServer.savedObjects.get({
|
||||
type: pluginStateSavedObjectsType,
|
||||
id: pluginStateSavedObjectsId,
|
||||
});
|
||||
|
||||
expect(createdSO.attributes.status).to.eql('in_progress');
|
||||
});
|
||||
|
||||
it('creates a guide saved object when updating the guide and there is no guide SO yet', async () => {
|
||||
await supertest
|
||||
.put(putStatePath)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
guide: testGuideStep1ActiveState,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const createdSO = await kibanaServer.savedObjects.get({
|
||||
type: guideStateSavedObjectsType,
|
||||
id: testGuide,
|
||||
});
|
||||
|
||||
expect(createdSO.attributes).to.eql(testGuideStep1ActiveState);
|
||||
});
|
||||
|
||||
it('updates the guide saved object when updating the guide and there is already guide SO', async () => {
|
||||
await createGuides(kibanaServer, [testGuideStep1ActiveState]);
|
||||
|
||||
await supertest
|
||||
.put(putStatePath)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
guide: testGuideNotActiveState,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const createdSO = await kibanaServer.savedObjects.get({
|
||||
type: guideStateSavedObjectsType,
|
||||
id: testGuide,
|
||||
});
|
||||
|
||||
expect(createdSO.attributes).to.eql(testGuideNotActiveState);
|
||||
});
|
||||
|
||||
it('updates any existing active guides to inactive', async () => {
|
||||
// create an active guide and an inactive guide
|
||||
await createGuides(kibanaServer, [
|
||||
testGuideStep1ActiveState,
|
||||
{ ...testGuideNotActiveState, guideId: 'search' },
|
||||
]);
|
||||
|
||||
// Create a new guide with isActive: true
|
||||
await supertest
|
||||
.put(putStatePath)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
guide: {
|
||||
...testGuideStep1ActiveState,
|
||||
guideId: 'observability',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// Check that all guides except observability are inactive
|
||||
const testGuideSO = await kibanaServer.savedObjects.get({
|
||||
type: guideStateSavedObjectsType,
|
||||
id: testGuide,
|
||||
});
|
||||
expect(testGuideSO.attributes.isActive).to.eql(false);
|
||||
|
||||
const searchGuideSO = await kibanaServer.savedObjects.get({
|
||||
type: guideStateSavedObjectsType,
|
||||
id: 'search',
|
||||
});
|
||||
expect(searchGuideSO.attributes.isActive).to.eql(false);
|
||||
|
||||
const observabilityGuide = await kibanaServer.savedObjects.get({
|
||||
type: guideStateSavedObjectsType,
|
||||
id: 'observability',
|
||||
});
|
||||
expect(observabilityGuide.attributes.isActive).to.eql(true);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -30,5 +30,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./ui_counters'));
|
||||
loadTestFile(require.resolve('./unified_field_list'));
|
||||
loadTestFile(require.resolve('./telemetry'));
|
||||
loadTestFile(require.resolve('./guided_onboarding'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,69 +0,0 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import {
|
||||
testGuideStep1ActiveState,
|
||||
testGuideNotActiveState,
|
||||
} from '@kbn/guided-onboarding-plugin/public/services/api.mocks';
|
||||
import { guidedSetupSavedObjectsType } from '@kbn/guided-onboarding-plugin/server/saved_objects/guided_setup';
|
||||
import type { GuideState } from '@kbn/guided-onboarding';
|
||||
import type { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
const mockSearchGuideNotActiveState: GuideState = {
|
||||
...testGuideNotActiveState,
|
||||
guideId: 'search',
|
||||
};
|
||||
|
||||
export default function testGetState({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
|
||||
describe('GET /api/guided_onboarding/state', () => {
|
||||
afterEach(async () => {
|
||||
// Clean up saved objects
|
||||
await kibanaServer.savedObjects.clean({ types: [guidedSetupSavedObjectsType] });
|
||||
});
|
||||
|
||||
const createGuides = async (guides: GuideState[]) => {
|
||||
for (const guide of guides) {
|
||||
await kibanaServer.savedObjects.create({
|
||||
type: guidedSetupSavedObjectsType,
|
||||
id: guide.guideId,
|
||||
overwrite: true,
|
||||
attributes: guide,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
it('should return the state for all guides', async () => {
|
||||
// Create two guides to return
|
||||
await createGuides([testGuideStep1ActiveState, mockSearchGuideNotActiveState]);
|
||||
|
||||
const response = await supertest.get('/api/guided_onboarding/state').expect(200);
|
||||
expect(response.body.state.length).to.eql(2);
|
||||
expect(response.body).to.eql({
|
||||
state: [testGuideStep1ActiveState, mockSearchGuideNotActiveState],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the state for the active guide with query param `active=true`', async () => {
|
||||
await createGuides([testGuideStep1ActiveState, mockSearchGuideNotActiveState]);
|
||||
|
||||
const response = await supertest
|
||||
.get('/api/guided_onboarding/state')
|
||||
.query({ active: true })
|
||||
.expect(200);
|
||||
expect(response.body).to.eql({ state: [testGuideStep1ActiveState] });
|
||||
});
|
||||
|
||||
it("should return an empty array if saved object doesn't exist", async () => {
|
||||
const response = await supertest.get('/api/guided_onboarding/state').expect(200);
|
||||
expect(response.body).to.eql({ state: [] });
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,128 +0,0 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import {
|
||||
testGuideStep1ActiveState,
|
||||
testGuideNotActiveState,
|
||||
} from '@kbn/guided-onboarding-plugin/public/services/api.mocks';
|
||||
import { guidedSetupSavedObjectsType } from '@kbn/guided-onboarding-plugin/server/saved_objects/guided_setup';
|
||||
import type { GuideState } from '@kbn/guided-onboarding';
|
||||
import type { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
const mockSearchGuideNotActiveState: GuideState = {
|
||||
...testGuideNotActiveState,
|
||||
guideId: 'search',
|
||||
};
|
||||
|
||||
export default function testPutState({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
|
||||
describe('PUT /api/guided_onboarding/state', () => {
|
||||
afterEach(async () => {
|
||||
// Clean up saved objects
|
||||
await kibanaServer.savedObjects.clean({ types: [guidedSetupSavedObjectsType] });
|
||||
});
|
||||
|
||||
it('should update a guide that has an existing saved object', async () => {
|
||||
// Create a saved object for the guide
|
||||
await kibanaServer.savedObjects.create({
|
||||
type: guidedSetupSavedObjectsType,
|
||||
id: testGuideStep1ActiveState.guideId,
|
||||
overwrite: true,
|
||||
attributes: testGuideStep1ActiveState,
|
||||
});
|
||||
|
||||
// Update the state of the guide
|
||||
await supertest
|
||||
.put(`/api/guided_onboarding/state`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
...testGuideStep1ActiveState,
|
||||
status: 'complete',
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// Check that the guide was updated
|
||||
const response = await supertest.get('/api/guided_onboarding/state').expect(200);
|
||||
const [updatedGuide] = response.body.state;
|
||||
expect(updatedGuide.status).to.eql('complete');
|
||||
});
|
||||
|
||||
it('should update a guide that does not have a saved object', async () => {
|
||||
await supertest
|
||||
.put(`/api/guided_onboarding/state`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
...testGuideStep1ActiveState,
|
||||
status: 'ready_to_complete',
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// Check that the guide was updated
|
||||
const response = await supertest.get('/api/guided_onboarding/state').expect(200);
|
||||
const [updatedGuide] = response.body.state;
|
||||
expect(updatedGuide.status).to.eql('ready_to_complete');
|
||||
});
|
||||
|
||||
it('should update any existing active guides to inactive', async () => {
|
||||
// Create an active guide
|
||||
await supertest
|
||||
.put(`/api/guided_onboarding/state`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
...testGuideStep1ActiveState,
|
||||
isActive: true,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// Create an inactive guide
|
||||
await supertest
|
||||
.put(`/api/guided_onboarding/state`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
...mockSearchGuideNotActiveState,
|
||||
isActive: false,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// Create a new guide with isActive: true
|
||||
await supertest
|
||||
.put(`/api/guided_onboarding/state`)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
guideId: 'observability',
|
||||
isActive: true,
|
||||
status: 'in_progress',
|
||||
steps: [
|
||||
{
|
||||
id: 'step1',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: 'step2',
|
||||
status: 'inactive',
|
||||
},
|
||||
{
|
||||
id: 'step3',
|
||||
status: 'inactive',
|
||||
},
|
||||
],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// Check that the active guide was updated
|
||||
const response = await supertest.get('/api/guided_onboarding/state').expect(200);
|
||||
const guides = response.body.state;
|
||||
expect(guides.length).to.eql(3);
|
||||
const activeGuides = guides.filter((guide: { isActive: boolean }) => guide.isActive);
|
||||
expect(activeGuides.length).to.eql(1);
|
||||
expect(activeGuides[0].guideId).to.eql('observability');
|
||||
});
|
||||
});
|
||||
}
|
|
@ -39,6 +39,5 @@ export default function ({ loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./cases'));
|
||||
loadTestFile(require.resolve('./monitoring_collection'));
|
||||
loadTestFile(require.resolve('./cloud_security_posture'));
|
||||
loadTestFile(require.resolve('./guided_onboarding'));
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue