[Guided onboarding] Add guide config route (#146149)

## Summary
Fixes https://github.com/elastic/kibana/issues/145871
Fixes https://github.com/elastic/kibana/issues/145875
This PR adds an internal API endpoint that returns all existing config
guides. The client side code (api service) is updated to use the
endpoint instead of a guide configs file.

Note: This PR deletes the docs link for the kube-state-metrics from the
"Add data" step in the Kubernetes guide. I opened
https://github.com/elastic/kibana/issues/146404 to follow up on this.

### Checklist

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

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Yulia Čech 2022-12-01 09:44:20 +01:00 committed by GitHub
parent 9e6f24a331
commit e7da574c5d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 723 additions and 206 deletions

View file

@ -27,7 +27,6 @@ import {
} from '@elastic/eui';
import type { GuideState, GuideStepIds, GuideId, GuideStep } from '@kbn/guided-onboarding';
import type { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/public';
import { guidesConfig } from '@kbn/guided-onboarding-plugin/public';
interface MainProps {
guidedOnboarding: GuidedOnboardingPluginStart;
@ -75,7 +74,15 @@ export const Main = (props: MainProps) => {
};
const updateGuideState = async () => {
const selectedGuideConfig = guidesConfig[selectedGuide!];
if (!selectedGuide) {
return;
}
const selectedGuideConfig = await guidedOnboardingApi?.getGuideConfig(selectedGuide);
if (!selectedGuideConfig) {
return;
}
const selectedStepIndex = selectedGuideConfig.steps.findIndex(
(step) => step.id === selectedStep!
);
@ -199,7 +206,7 @@ export const Main = (props: MainProps) => {
</EuiText>
<EuiSpacer />
<EuiFlexGroup>
{(Object.keys(guidesConfig) as GuideId[]).map((guideId) => {
{(['search', 'security', 'observability', 'testGuide'] as GuideId[]).map((guideId) => {
const guideState = guidesState?.find((guide) => guide.guideId === guideId);
return (
<EuiFlexItem>

View file

@ -0,0 +1,11 @@
/*
* 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 { PLUGIN_ID, PLUGIN_NAME, API_BASE_PATH } from './constants';
export { testGuideConfig } from './test_guide_config';
export type { PluginStatus, PluginState, StepConfig, GuideConfig, GuidesConfig } from './types';

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import type { GuideConfig } from '../../types';
import type { GuideConfig } from './types';
export const testGuideConfig: GuideConfig = {
title: 'Test guide for development',

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { GuideState } from '@kbn/guided-onboarding';
import type { GuideId, GuideState, GuideStepIds, StepStatus } from '@kbn/guided-onboarding';
/**
* Guided onboarding overall status:
@ -31,3 +31,42 @@ export interface PluginState {
isActivePeriod: boolean;
activeGuide?: GuideState;
}
export interface StepConfig {
id: GuideStepIds;
title: string;
// description is displayed as a single paragraph, can be combined with description list
description?: string;
// description list is displayed as an unordered list, can be combined with description
descriptionList?: Array<string | React.ReactNode>;
location?: {
appID: string;
path: string;
};
status?: StepStatus;
integration?: string;
manualCompletion?: {
title: string;
description: string;
readyToCompleteOnNavigation?: boolean;
};
}
export interface GuideConfig {
title: string;
description: string;
guideName: string;
docs?: {
text: string;
url: string;
};
completedGuideRedirectLocation?: {
appID: string;
path: string;
};
steps: StepConfig[];
}
export type GuidesConfig = {
[key in GuideId]: GuideConfig;
};

View file

@ -11,12 +11,12 @@ 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 type { GuideConfig, PluginState } from '../../common';
import { GuideButtonPopover } from './guide_button_popover';
interface GuideButtonProps {
pluginState: PluginState | undefined;
guideConfig: GuideConfig | undefined;
toggleGuidePanel: () => void;
isGuidePanelOpen: boolean;
navigateToLandingPage: () => void;
@ -42,6 +42,7 @@ const getStepNumber = (state: GuideState): number | undefined => {
export const GuideButton = ({
pluginState,
guideConfig,
toggleGuidePanel,
isGuidePanelOpen,
navigateToLandingPage,
@ -101,7 +102,7 @@ export const GuideButton = ({
</EuiButton>
);
if (stepReadyToComplete) {
const stepConfig = getStepConfig(pluginState.activeGuide.guideId, stepReadyToComplete.id);
const stepConfig = guideConfig?.steps.find((step) => step.id === stepReadyToComplete.id);
// check if the stepConfig has manualCompletion info
if (stepConfig && stepConfig.manualCompletion) {
return (

View file

@ -15,11 +15,12 @@ import { httpServiceMock } from '@kbn/core/public/mocks';
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 type { PluginState } from '../../common';
import { API_BASE_PATH, testGuideConfig } from '../../common';
import { apiService } from '../services/api';
import type { GuidedOnboardingApi } from '../types';
import {
testGuide,
testGuideStep1ActiveState,
testGuideStep1InProgressState,
testGuideStep2InProgressState,
@ -33,13 +34,21 @@ import { GuidePanel } from './guide_panel';
const applicationMock = applicationServiceMock.createStartContract();
const notificationsMock = notificationServiceMock.createStartContract();
const mockGetResponse = (path: string, pluginState: PluginState) => {
if (path === `${API_BASE_PATH}/configs/${testGuide}`) {
return Promise.resolve({
config: testGuideConfig,
});
}
return Promise.resolve({ pluginState });
};
const setupComponentWithPluginStateMock = async (
httpClient: jest.Mocked<HttpSetup>,
pluginState: PluginState
) => {
httpClient.get.mockResolvedValue({
pluginState,
});
httpClient.get.mockImplementation((path) =>
mockGetResponse(path as unknown as string, pluginState)
);
apiService.setup(httpClient, true);
return await setupGuidePanelComponent(apiService);
};
@ -232,7 +241,7 @@ describe('Guided setup', () => {
expect(exists('guidePanel')).toBe(true);
expect(exists('guideProgress')).toBe(false);
expect(find('guidePanelStep').length).toEqual(guidesConfig.testGuide.steps.length);
expect(find('guidePanelStep').length).toEqual(testGuideConfig.steps.length);
});
describe('Guide completion', () => {
@ -423,7 +432,7 @@ describe('Guided setup', () => {
expect(
find('guidePanelStepDescription')
.last()
.containsMatchingElement(<p>{guidesConfig.testGuide.steps[2].description}</p>)
.containsMatchingElement(<p>{testGuideConfig.steps[2].description}</p>)
).toBe(true);
});
@ -441,7 +450,7 @@ describe('Guided setup', () => {
.first()
.containsMatchingElement(
<ul>
{guidesConfig.testGuide.steps[0].descriptionList?.map((description, i) => (
{testGuideConfig.steps[0].descriptionList?.map((description, i) => (
<li key={i}>{description}</li>
))}
</ul>

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import {
EuiFlyout,
EuiFlyoutBody,
@ -32,10 +32,9 @@ import { ApplicationStart, NotificationsStart } from '@kbn/core/public';
import type { GuideState, GuideStep as GuideStepStatus } from '@kbn/guided-onboarding';
import { GuideId } from '@kbn/guided-onboarding';
import type { GuideConfig, GuidedOnboardingApi, StepConfig } from '../types';
import type { GuidedOnboardingApi } from '../types';
import type { PluginState } from '../../common/types';
import { getGuideConfig } from '../services/helpers';
import type { GuideConfig, PluginState, StepConfig } from '../../common';
import { GuideStep } from './guide_panel_step';
import { QuitGuideModal } from './quit_guide_modal';
@ -79,6 +78,7 @@ export const GuidePanel = ({ api, application, notifications }: GuidePanelProps)
const [isGuideOpen, setIsGuideOpen] = useState(false);
const [isQuitGuideModalOpen, setIsQuitGuideModalOpen] = useState(false);
const [pluginState, setPluginState] = useState<PluginState | undefined>(undefined);
const [guideConfig, setGuideConfig] = useState<GuideConfig | undefined>(undefined);
const styles = getGuidePanelStyles(euiTheme);
@ -170,7 +170,16 @@ export const GuidePanel = ({ api, application, notifications }: GuidePanelProps)
return () => subscription.unsubscribe();
}, [api]);
const guideConfig = getGuideConfig(pluginState?.activeGuide?.guideId)!;
const fetchGuideConfig = useCallback(async () => {
if (pluginState?.activeGuide?.guideId) {
const config = await api.getGuideConfig(pluginState.activeGuide.guideId);
if (config) setGuideConfig(config);
}
}, [api, pluginState]);
useEffect(() => {
fetchGuideConfig();
}, [fetchGuideConfig]);
// TODO handle loading state
// https://github.com/elastic/kibana/issues/139799
@ -184,6 +193,7 @@ export const GuidePanel = ({ api, application, notifications }: GuidePanelProps)
<div css={styles.setupButton}>
<GuideButton
pluginState={pluginState}
guideConfig={guideConfig}
toggleGuidePanel={toggleGuide}
isGuidePanelOpen={isGuideOpen}
navigateToLandingPage={navigateToLandingPage}

View file

@ -22,7 +22,7 @@ import {
import { i18n } from '@kbn/i18n';
import type { StepStatus } from '@kbn/guided-onboarding';
import type { StepConfig } from '../types';
import type { StepConfig } from '../../common';
import { getGuidePanelStepStyles } from './guide_panel_step.styles';
interface GuideStepProps {

View file

@ -16,5 +16,3 @@ export type {
GuidedOnboardingPluginStart,
GuidedOnboardingApi,
} from './types';
export { guidesConfig } from './constants/guides_config';

View file

@ -25,6 +25,7 @@ const apiServiceMock: jest.Mocked<GuidedOnboardingPluginStart> = {
completeGuidedOnboardingForIntegration: jest.fn(),
skipGuidedOnboarding: jest.fn(),
isGuidePanelOpen$: new BehaviorSubject(false),
getGuideConfig: jest.fn(),
},
};

View file

@ -8,7 +8,7 @@
import type { GuideState, GuideId, GuideStepIds } from '@kbn/guided-onboarding';
import { PluginState } from '../../common/types';
import { PluginState } from '../../common';
export const testGuide: GuideId = 'testGuide';
export const testGuideFirstStep: GuideStepIds = 'step1';
@ -87,7 +87,7 @@ export const testGuideStep2ReadyToCompleteState: GuideState = {
status: 'complete',
},
{
id: testGuideStep1ActiveState.steps[1].id,
...testGuideStep1ActiveState.steps[1],
status: 'ready_to_complete',
},
testGuideStep1ActiveState.steps[2],

View file

@ -11,7 +11,7 @@ import { httpServiceMock } from '@kbn/core/public/mocks';
import type { GuideState } from '@kbn/guided-onboarding';
import { firstValueFrom, Subscription } from 'rxjs';
import { API_BASE_PATH } from '../../common/constants';
import { API_BASE_PATH, testGuideConfig } from '../../common';
import { ApiService } from './api';
import {
testGuide,
@ -129,9 +129,9 @@ describe('GuidedOnboarding ApiService', () => {
describe('activateGuide', () => {
it('activates a new guide', async () => {
// update the mock to no active guides
httpClient.get.mockResolvedValue({
pluginState: mockPluginStateNotStarted,
// mock the get config request
httpClient.get.mockResolvedValueOnce({
config: testGuideConfig,
});
apiService.setup(httpClient, true);
@ -305,9 +305,12 @@ describe('GuidedOnboarding ApiService', () => {
});
it(`marks the step as 'ready_to_complete' if it's configured for manual completion`, async () => {
httpClient.get.mockResolvedValue({
httpClient.get.mockResolvedValueOnce({
pluginState: { ...mockPluginStateInProgress, activeGuide: testGuideStep2InProgressState },
});
httpClient.get.mockResolvedValueOnce({
config: testGuideConfig,
});
apiService.setup(httpClient, true);
await apiService.completeGuideStep(testGuide, testGuideManualCompletionStep);
@ -329,7 +332,7 @@ describe('GuidedOnboarding ApiService', () => {
});
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({
httpClient.get.mockResolvedValueOnce({
pluginState: {
...mockPluginStateInProgress,
activeGuide: {
@ -341,6 +344,9 @@ describe('GuidedOnboarding ApiService', () => {
},
},
});
httpClient.get.mockResolvedValueOnce({
config: testGuideConfig,
});
apiService.setup(httpClient, true);
await apiService.completeGuideStep(testGuide, testGuideLastStep);
@ -402,9 +408,12 @@ describe('GuidedOnboarding ApiService', () => {
describe('isGuidedOnboardingActiveForIntegration$', () => {
it('returns true if the integration is part of the active step', (done) => {
httpClient.get.mockResolvedValue({
httpClient.get.mockResolvedValueOnce({
pluginState: { ...mockPluginStateInProgress, activeGuide: testGuideStep1InProgressState },
});
httpClient.get.mockResolvedValueOnce({
config: testGuideConfig,
});
apiService.setup(httpClient, true);
subscription = apiService
.isGuidedOnboardingActiveForIntegration$(testIntegration)
@ -449,9 +458,12 @@ describe('GuidedOnboarding ApiService', () => {
describe('completeGuidedOnboardingForIntegration', () => {
it(`completes the step if it's active for the integration`, async () => {
httpClient.get.mockResolvedValue({
httpClient.get.mockResolvedValueOnce({
pluginState: { ...mockPluginStateInProgress, activeGuide: testGuideStep1InProgressState },
});
httpClient.get.mockResolvedValueOnce({
config: testGuideConfig,
});
apiService.setup(httpClient, true);
await apiService.completeGuidedOnboardingForIntegration(testIntegration);
@ -482,6 +494,26 @@ describe('GuidedOnboarding ApiService', () => {
});
});
describe('skipGuidedOnboarding', () => {
it(`sends a request to the put state API`, async () => {
await apiService.skipGuidedOnboarding();
expect(httpClient.put).toHaveBeenCalledTimes(1);
// this assertion depends on the guides config
expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
body: JSON.stringify({ status: 'skipped' }),
});
});
});
describe('getGuideConfig', () => {
it('sends a request to the get config API', async () => {
apiService.setup(httpClient, true);
await apiService.getGuideConfig(testGuide);
expect(httpClient.get).toHaveBeenCalledTimes(1);
expect(httpClient.get).toHaveBeenCalledWith(`${API_BASE_PATH}/configs/${testGuide}`);
});
});
describe('no API requests are sent on self-managed deployments', () => {
beforeEach(() => {
apiService.setup(httpClient, false);
@ -501,5 +533,10 @@ describe('GuidedOnboarding ApiService', () => {
await apiService.updatePluginState({}, false);
expect(httpClient.put).not.toHaveBeenCalled();
});
it('getGuideConfig', async () => {
await apiService.getGuideConfig(testGuide);
expect(httpClient.get).not.toHaveBeenCalled();
});
});
});

View file

@ -7,23 +7,31 @@
*/
import { HttpSetup } from '@kbn/core/public';
import { BehaviorSubject, map, Observable, firstValueFrom, concat, of } from 'rxjs';
import {
BehaviorSubject,
map,
Observable,
firstValueFrom,
concatMap,
of,
concat,
from,
} from 'rxjs';
import type { GuideState, GuideId, GuideStep, GuideStepIds } from '@kbn/guided-onboarding';
import { API_BASE_PATH } from '../../common/constants';
import { PluginState, PluginStatus } from '../../common/types';
import { API_BASE_PATH } from '../../common';
import type { PluginState, PluginStatus, GuideConfig } from '../../common';
import { GuidedOnboardingApi } from '../types';
import {
getGuideConfig,
getInProgressStepId,
getStepConfig,
getUpdatedSteps,
getGuideStatusOnStepCompletion,
isIntegrationInGuideStep,
getCompletedSteps,
isStepInProgress,
isStepReadyToComplete,
isGuideActive,
getStepConfig,
isLastStep,
} from './helpers';
import { ConfigService } from './config_service';
export class ApiService implements GuidedOnboardingApi {
private isCloudEnabled: boolean | undefined;
@ -31,12 +39,14 @@ export class ApiService implements GuidedOnboardingApi {
private pluginState$!: BehaviorSubject<PluginState | undefined>;
private isPluginStateLoading: boolean | undefined;
public isGuidePanelOpen$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
private configService = new ConfigService();
public setup(httpClient: HttpSetup, isCloudEnabled: boolean) {
this.isCloudEnabled = isCloudEnabled;
this.client = httpClient;
this.pluginState$ = new BehaviorSubject<PluginState | undefined>(undefined);
this.isGuidePanelOpen$ = new BehaviorSubject<boolean>(false);
this.configService.setup(httpClient);
}
private createGetPluginStateObservable(): Observable<PluginState | undefined> {
@ -175,7 +185,7 @@ export class ApiService implements GuidedOnboardingApi {
}
// If this is the 1st-time attempt, we need to create the default state
const guideConfig = getGuideConfig(guideId);
const guideConfig = await this.configService.getGuideConfig(guideId);
if (guideConfig) {
const updatedSteps: GuideStep[] = guideConfig.steps.map((step, stepIndex) => {
@ -242,8 +252,7 @@ export class ApiService implements GuidedOnboardingApi {
// All steps should be complete at this point
// However, we do a final check here as a safeguard
const allStepsComplete =
Boolean(activeGuide!.steps.find((step) => step.status !== 'complete')) === false;
const allStepsComplete = Boolean(activeGuide!.steps.find((step) => step.status === 'complete'));
if (allStepsComplete) {
const updatedGuide: GuideState = {
@ -335,11 +344,13 @@ export class ApiService implements GuidedOnboardingApi {
const isCurrentStepInProgress = isStepInProgress(activeGuide, guideId, stepId);
const isCurrentStepReadyToComplete = isStepReadyToComplete(activeGuide, guideId, stepId);
const stepConfig = getStepConfig(activeGuide!.guideId, stepId);
const guideConfig = await this.configService.getGuideConfig(guideId);
const stepConfig = getStepConfig(guideConfig, activeGuide!.guideId, stepId);
const isManualCompletion = stepConfig ? !!stepConfig.manualCompletion : false;
const isLastStepInGuide = isLastStep(guideConfig, guideId, stepId);
if (isCurrentStepInProgress || isCurrentStepReadyToComplete) {
const updatedSteps = getUpdatedSteps(
const updatedSteps = getCompletedSteps(
activeGuide!,
stepId,
// if current step is in progress and configured for manual completion,
@ -347,10 +358,15 @@ export class ApiService implements GuidedOnboardingApi {
isManualCompletion && isCurrentStepInProgress
);
const status = await this.configService.getGuideStatusOnStepCompletion({
isLastStepInGuide,
isManualCompletion,
isStepReadyToComplete: isCurrentStepReadyToComplete,
});
const currentGuide: GuideState = {
guideId,
isActive: true,
status: getGuideStatusOnStepCompletion(activeGuide, guideId, stepId),
status,
steps: updatedSteps,
};
@ -377,7 +393,9 @@ export class ApiService implements GuidedOnboardingApi {
*/
public isGuidedOnboardingActiveForIntegration$(integration?: string): Observable<boolean> {
return this.fetchPluginState$().pipe(
map((state) => isIntegrationInGuideStep(state?.activeGuide, integration))
concatMap((state) =>
from(this.configService.isIntegrationInGuideStep(state?.activeGuide, integration))
)
);
}
@ -396,7 +414,10 @@ export class ApiService implements GuidedOnboardingApi {
const { activeGuide } = pluginState!;
const inProgressStepId = getInProgressStepId(activeGuide!);
if (!inProgressStepId) return undefined;
const isIntegrationStepActive = isIntegrationInGuideStep(activeGuide!, integration);
const isIntegrationStepActive = await this.configService.isIntegrationInGuideStep(
activeGuide!,
integration
);
if (isIntegrationStepActive) {
return await this.completeGuideStep(activeGuide!.guideId, inProgressStepId);
}
@ -410,6 +431,20 @@ export class ApiService implements GuidedOnboardingApi {
public async skipGuidedOnboarding(): Promise<{ pluginState: PluginState } | undefined> {
return await this.updatePluginState({ status: 'skipped' }, false);
}
/**
* Gets the config for the guide.
* @return {Promise} a promise with the guide config or undefined if the config is not found
*/
public async getGuideConfig(guideId: GuideId): Promise<GuideConfig | undefined> {
if (!this.isCloudEnabled) {
return undefined;
}
if (!this.client) {
throw new Error('ApiService has not be initialized.');
}
return await this.configService.getGuideConfig(guideId);
}
}
export const apiService = new ApiService();

View file

@ -0,0 +1,119 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { HttpSetup } from '@kbn/core-http-browser';
import { httpServiceMock } from '@kbn/core-http-browser-mocks';
import { API_BASE_PATH, testGuideConfig } from '../../common';
import {
testGuide,
testGuideNotActiveState,
testGuideStep1InProgressState,
testGuideStep2InProgressState,
testIntegration,
wrongIntegration,
} from './api.mocks';
import { ConfigService } from './config_service';
describe('GuidedOnboarding ConfigService', () => {
let configService: ConfigService;
let httpClient: jest.Mocked<HttpSetup>;
beforeEach(() => {
httpClient = httpServiceMock.createStartContract({ basePath: '/base/path' });
httpClient.get.mockResolvedValue({
config: testGuideConfig,
});
configService = new ConfigService();
configService.setup(httpClient);
});
describe('getGuideConfig', () => {
it('sends only one request to the get configs API', async () => {
await configService.getGuideConfig(testGuide);
await configService.getGuideConfig(testGuide);
expect(httpClient.get).toHaveBeenCalledTimes(1);
expect(httpClient.get).toHaveBeenCalledWith(`${API_BASE_PATH}/configs/${testGuide}`);
});
it('returns undefined if the config is not found', async () => {
httpClient.get.mockRejectedValueOnce(new Error('Not found'));
configService.setup(httpClient);
const config = await configService.getGuideConfig(testGuide);
expect(config).toBeUndefined();
});
it('returns the config for the guide', async () => {
const config = await configService.getGuideConfig(testGuide);
expect(config).toHaveProperty('title');
});
});
describe('getGuideStatusOnStepCompletion', () => {
it('returns in_progress when completing not the last step', async () => {
const status = await configService.getGuideStatusOnStepCompletion({
isLastStepInGuide: false,
isManualCompletion: true,
isStepReadyToComplete: true,
});
expect(status).toBe('in_progress');
});
it('when completing the last step that is configured for manual completion, returns in_progress if the step is in progress', async () => {
const status = await configService.getGuideStatusOnStepCompletion({
isLastStepInGuide: true,
isManualCompletion: true,
isStepReadyToComplete: false,
});
expect(status).toBe('in_progress');
});
it('when completing the last step that is configured for manual completion, returns ready_to_complete if the step is ready_to_complete', async () => {
const status = await configService.getGuideStatusOnStepCompletion({
isLastStepInGuide: true,
isManualCompletion: true,
isStepReadyToComplete: true,
});
expect(status).toBe('ready_to_complete');
});
});
describe('isIntegrationInGuideStep', () => {
it('return true if the integration is defined in the guide step config', async () => {
const result = await configService.isIntegrationInGuideStep(
testGuideStep1InProgressState,
testIntegration
);
expect(result).toBe(true);
});
it('returns false if a different integration is defined in the guide step', async () => {
const result = await configService.isIntegrationInGuideStep(
testGuideStep1InProgressState,
wrongIntegration
);
expect(result).toBe(false);
});
it('returns false if no integration is defined in the guide step', async () => {
const result = await configService.isIntegrationInGuideStep(
testGuideStep2InProgressState,
testIntegration
);
expect(result).toBe(false);
});
it('returns false if no guide is active', async () => {
const result = await configService.isIntegrationInGuideStep(
testGuideNotActiveState,
testIntegration
);
expect(result).toBe(false);
});
it('returns false if no integration passed', async () => {
const result = await configService.isIntegrationInGuideStep(testGuideStep1InProgressState);
expect(result).toBe(false);
});
});
});

View file

@ -0,0 +1,86 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { HttpSetup } from '@kbn/core-http-browser';
import { GuideId, GuideState, GuideStatus } from '@kbn/guided-onboarding';
import type { GuideConfig, GuidesConfig } from '../../common';
import { API_BASE_PATH } from '../../common';
import { findGuideConfigByGuideId, getInProgressStepConfig } from './helpers';
type ConfigInitialization = {
[key in GuideId]: boolean | undefined;
};
export class ConfigService {
private client: HttpSetup | undefined;
private configs: GuidesConfig | undefined;
private isConfigInitialized: ConfigInitialization | undefined;
setup(httpClient: HttpSetup) {
this.client = httpClient;
this.configs = {} as GuidesConfig;
this.isConfigInitialized = {} as ConfigInitialization;
}
public async getGuideConfig(guideId: GuideId): Promise<GuideConfig | undefined> {
if (!this.client) {
throw new Error('ConfigService has not be initialized.');
}
// if not initialized yet, get the config from the backend
if (!this.isConfigInitialized || !this.isConfigInitialized[guideId]) {
try {
const { config } = await this.client.get<{ config: GuideConfig }>(
`${API_BASE_PATH}/configs/${guideId}`
);
if (!this.isConfigInitialized) this.isConfigInitialized = {} as ConfigInitialization;
this.isConfigInitialized[guideId] = true;
if (!this.configs) this.configs = {} as GuidesConfig;
this.configs[guideId] = config;
} catch (e) {
// if there is an error, set the isInitialized property to avoid multiple requests
if (!this.isConfigInitialized) this.isConfigInitialized = {} as ConfigInitialization;
this.isConfigInitialized[guideId] = true;
}
}
// get the config from the configs property
return findGuideConfigByGuideId(this.configs, guideId);
}
public async getGuideStatusOnStepCompletion({
isLastStepInGuide,
isManualCompletion,
isStepReadyToComplete,
}: {
isLastStepInGuide: boolean;
isManualCompletion: boolean;
isStepReadyToComplete: boolean;
}): Promise<GuideStatus> {
// We want to set the guide status to 'ready_to_complete' if the current step is the last step in the guide
// and the step is not configured for manual completion
// or if the current step is configured for manual completion and the last step is ready to complete
if (
(isLastStepInGuide && !isManualCompletion) ||
(isLastStepInGuide && isManualCompletion && isStepReadyToComplete)
) {
return 'ready_to_complete';
}
// Otherwise the guide is still in progress
return 'in_progress';
}
public async isIntegrationInGuideStep(
guideState?: GuideState,
integration?: string
): Promise<boolean> {
if (!guideState || !guideState.isActive) return false;
const guideConfig = await this.getGuideConfig(guideState.guideId);
const stepConfig = getInProgressStepConfig(guideConfig, guideState);
return stepConfig ? stepConfig.integration === integration : false;
}
}

View file

@ -6,51 +6,219 @@
* Side Public License, v 1.
*/
import { isIntegrationInGuideStep, isLastStep } from './helpers';
import { testGuideConfig } from '../../common';
import type { GuidesConfig } from '../../common';
import {
findGuideConfigByGuideId,
getCompletedSteps,
getInProgressStepConfig,
getInProgressStepId,
getStepConfig,
isGuideActive,
isLastStep,
isStepInProgress,
isStepReadyToComplete,
} from './helpers';
import {
mockPluginStateInProgress,
mockPluginStateNotStarted,
testGuide,
testGuideFirstStep,
testGuideLastStep,
testGuideNotActiveState,
testGuideManualCompletionStep,
testGuideStep1ActiveState,
testGuideStep1InProgressState,
testGuideStep2InProgressState,
testIntegration,
wrongIntegration,
testGuideStep2ReadyToCompleteState,
} from './api.mocks';
describe('GuidedOnboarding ApiService helpers', () => {
describe('isLastStepActive', () => {
describe('findGuideConfigByGuideId', () => {
it('returns undefined if the config is not found', () => {
const config = findGuideConfigByGuideId(
{ testGuide: testGuideConfig } as GuidesConfig,
'security'
);
expect(config).toBeUndefined();
});
it('returns the correct config guide', () => {
const config = findGuideConfigByGuideId(
{ testGuide: testGuideConfig } as GuidesConfig,
testGuide
);
expect(config).not.toBeUndefined();
});
});
describe('getStepConfig', () => {
it('returns undefined if the config is not found', async () => {
const config = getStepConfig(undefined, testGuide, testGuideFirstStep);
expect(config).toBeUndefined();
});
it('returns the config for the step', async () => {
const config = getStepConfig(testGuideConfig, testGuide, testGuideFirstStep);
expect(config).toHaveProperty('title');
});
});
describe('isLastStep', () => {
it('returns true if the passed params are for the last step', () => {
const result = isLastStep(testGuide, testGuideLastStep);
const result = isLastStep(testGuideConfig, testGuide, testGuideLastStep);
expect(result).toBe(true);
});
it('returns false if the passed params are not for the last step', () => {
const result = isLastStep(testGuide, testGuideFirstStep);
const result = isLastStep(testGuideConfig, testGuide, testGuideFirstStep);
expect(result).toBe(false);
});
});
describe('isIntegrationInGuideStep', () => {
it('return true if the integration is defined in the guide step config', () => {
const result = isIntegrationInGuideStep(testGuideStep1InProgressState, testIntegration);
expect(result).toBe(true);
describe('getInProgressStepId', () => {
it('returns undefined if no step is in progress', () => {
const stepId = getInProgressStepId(testGuideStep1ActiveState);
expect(stepId).toBeUndefined();
});
it('returns false if a different integration is defined in the guide step', () => {
const result = isIntegrationInGuideStep(testGuideStep1InProgressState, wrongIntegration);
expect(result).toBe(false);
it('returns the correct step if that is in progress', () => {
const stepId = getInProgressStepId(testGuideStep1InProgressState);
expect(stepId).toBe('step1');
});
it('returns false if no integration is defined in the guide step', () => {
const result = isIntegrationInGuideStep(testGuideStep2InProgressState, testIntegration);
expect(result).toBe(false);
});
describe('getInProgressStepConfig', () => {
it('returns undefined if no guide config', () => {
const stepConfig = getInProgressStepConfig(undefined, testGuideStep1ActiveState);
expect(stepConfig).toBeUndefined();
});
it('returns false if no guide is active', () => {
const result = isIntegrationInGuideStep(testGuideNotActiveState, testIntegration);
expect(result).toBe(false);
it('returns undefined if no step is in progress', () => {
const stepConfig = getInProgressStepConfig(testGuideConfig, testGuideStep1ActiveState);
expect(stepConfig).toBeUndefined();
});
it('returns false if no integration passed', () => {
const result = isIntegrationInGuideStep(testGuideStep1InProgressState);
expect(result).toBe(false);
it('returns the correct step config for the step in progress', () => {
const stepConfig = getInProgressStepConfig(testGuideConfig, testGuideStep1InProgressState);
expect(stepConfig).toEqual(testGuideConfig.steps[0]);
});
});
describe('isGuideActive', () => {
it('returns false if plugin state is undefined', () => {
const isActive = isGuideActive(undefined, testGuide);
expect(isActive).toBe(false);
});
it('returns true if guideId is undefined and the guide is active', () => {
const isActive = isGuideActive(mockPluginStateInProgress, undefined);
expect(isActive).toBe(true);
});
it('returns false if guideId is undefined and the guide is not active', () => {
const isActive = isGuideActive(mockPluginStateNotStarted, undefined);
expect(isActive).toBe(false);
});
it('returns false if guide is not in progress', () => {
const isActive = isGuideActive(mockPluginStateInProgress, 'security');
expect(isActive).toBe(false);
});
it('returns true if guide is in progress', () => {
const isActive = isGuideActive(mockPluginStateInProgress, testGuide);
expect(isActive).toBe(true);
});
});
describe('isStepInProgress', () => {
it('returns false if guide state is undefined', () => {
const isInProgress = isStepInProgress(undefined, testGuide, testGuideFirstStep);
expect(isInProgress).toBe(false);
});
it('returns false if guide is not active', () => {
const isInProgress = isStepInProgress(
testGuideStep1InProgressState,
'security',
testGuideFirstStep
);
expect(isInProgress).toBe(false);
});
it('returns false if step is not in progress', () => {
const isInProgress = isStepInProgress(
testGuideStep1InProgressState,
testGuide,
testGuideLastStep
);
expect(isInProgress).toBe(false);
});
it('returns true if step is in progress', () => {
const isInProgress = isStepInProgress(
testGuideStep1InProgressState,
testGuide,
testGuideFirstStep
);
expect(isInProgress).toBe(true);
});
});
describe('isStepReadyToComplete', () => {
it('returns false if guide state is undefined', () => {
const isReadyToComplete = isStepReadyToComplete(undefined, testGuide, testGuideFirstStep);
expect(isReadyToComplete).toBe(false);
});
it('returns false if guide is not active', () => {
const isReadyToComplete = isStepReadyToComplete(
testGuideStep1InProgressState,
'security',
testGuideFirstStep
);
expect(isReadyToComplete).toBe(false);
});
it('returns false if step is not ready not complete', () => {
const isReadyToComplete = isStepReadyToComplete(
testGuideStep2ReadyToCompleteState,
testGuide,
testGuideLastStep
);
expect(isReadyToComplete).toBe(false);
});
it('returns true if step is ready to complete', () => {
const isInProgress = isStepReadyToComplete(
testGuideStep2ReadyToCompleteState,
testGuide,
testGuideManualCompletionStep
);
expect(isInProgress).toBe(true);
});
});
describe('getCompletedSteps', () => {
it('completes the step if setToReadyToComplete is false', () => {
const completedSteps = getCompletedSteps(
testGuideStep1InProgressState,
testGuideFirstStep,
false
);
expect(completedSteps[0].status).toBe('complete');
// the next step is active
expect(completedSteps[1].status).toBe('active');
});
it('sets the step to ready_to_complete if setToReadyToComplete is true', () => {
const completedSteps = getCompletedSteps(
testGuideStep2InProgressState,
testGuideManualCompletionStep,
true
);
expect(completedSteps[1].status).toBe('ready_to_complete');
// the next step is inactive
expect(completedSteps[2].status).toBe('inactive');
});
});
});

View file

@ -11,35 +11,45 @@ import type {
GuideStepIds,
GuideState,
GuideStep,
GuideStatus,
StepStatus,
} from '@kbn/guided-onboarding';
import { guidesConfig } from '../constants/guides_config';
import { GuideConfig, StepConfig } from '../types';
import type { PluginState } from '../../common/types';
import type { GuidesConfig, PluginState, GuideConfig, StepConfig } from '../../common';
export const getGuideConfig = (guideId?: GuideId): GuideConfig | undefined => {
if (guideId && Object.keys(guidesConfig).includes(guideId)) {
export const findGuideConfigByGuideId = (
guidesConfig?: GuidesConfig,
guideId?: GuideId
): GuideConfig | undefined => {
if (guidesConfig && guideId && Object.keys(guidesConfig).includes(guideId)) {
return guidesConfig[guideId];
}
};
export const getStepConfig = (guideId: GuideId, stepId: GuideStepIds): StepConfig | undefined => {
const guideConfig = getGuideConfig(guideId);
export const getStepConfig = (
guideConfig: GuideConfig | undefined,
guideId: GuideId,
stepId: GuideStepIds
): StepConfig | undefined => {
return guideConfig?.steps.find((step) => step.id === stepId);
};
const getStepIndex = (guideId: GuideId, stepId: GuideStepIds): number => {
const guide = getGuideConfig(guideId);
if (guide) {
return guide.steps.findIndex((step: StepConfig) => step.id === stepId);
const getStepIndex = (
guideConfig: GuideConfig | undefined,
guideId: GuideId,
stepId: GuideStepIds
): number => {
if (guideConfig) {
return guideConfig.steps.findIndex((step: StepConfig) => step.id === stepId);
}
return -1;
};
export const isLastStep = (guideId: GuideId, stepId: GuideStepIds): boolean => {
const guide = getGuideConfig(guideId);
const activeStepIndex = getStepIndex(guideId, stepId);
const stepsNumber = guide?.steps.length || 0;
export const isLastStep = (
guideConfig: GuideConfig | undefined,
guideId: GuideId,
stepId: GuideStepIds
): boolean => {
const activeStepIndex = getStepIndex(guideConfig, guideId, stepId);
const stepsNumber = guideConfig?.steps.length || 0;
if (stepsNumber > 0) {
return activeStepIndex === stepsNumber - 1;
}
@ -51,26 +61,18 @@ export const getInProgressStepId = (state: GuideState): GuideStepIds | undefined
return inProgressStep ? inProgressStep.id : undefined;
};
const getInProgressStepConfig = (state: GuideState): StepConfig | undefined => {
export const getInProgressStepConfig = (
guideConfig: GuideConfig | undefined,
state: GuideState
): StepConfig | undefined => {
const inProgressStepId = getInProgressStepId(state);
if (inProgressStepId) {
const config = getGuideConfig(state.guideId);
if (config) {
return config.steps.find((step) => step.id === inProgressStepId);
if (guideConfig) {
return guideConfig.steps.find((step) => step.id === inProgressStepId);
}
}
};
export const isIntegrationInGuideStep = (
guideState?: GuideState,
integration?: string
): boolean => {
if (!guideState || !guideState.isActive) return false;
const stepConfig = getInProgressStepConfig(guideState);
return stepConfig ? stepConfig.integration === integration : false;
};
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
@ -85,16 +87,24 @@ export const isGuideActive = (pluginState?: PluginState, guideId?: GuideId): boo
return true;
};
const isStepStatus = (
guideState: GuideState | undefined,
status: StepStatus,
guideId: GuideId,
stepId: GuideStepIds
): boolean => {
if (!guideState || !guideState.isActive || guideState.guideId !== guideId) return false;
// false if the step is not 'in_progress'
const selectedStep = guideState.steps.find((step) => step.id === stepId);
return selectedStep ? selectedStep.status === status : false;
};
export const isStepInProgress = (
guideState: GuideState | undefined,
guideId: GuideId,
stepId: GuideStepIds
): boolean => {
if (!guideState || !guideState.isActive) return false;
// false if the step is not 'in_progress'
const selectedStep = guideState.steps.find((step) => step.id === stepId);
return selectedStep ? selectedStep.status === 'in_progress' : false;
return isStepStatus(guideState, 'in_progress', guideId, stepId);
};
export const isStepReadyToComplete = (
@ -102,13 +112,10 @@ export const isStepReadyToComplete = (
guideId: GuideId,
stepId: GuideStepIds
): boolean => {
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;
return isStepStatus(guideState, 'ready_to_complete', guideId, stepId);
};
export const getUpdatedSteps = (
export const getCompletedSteps = (
guideState: GuideState,
stepId: GuideStepIds,
setToReadyToComplete?: boolean
@ -141,27 +148,3 @@ export const getUpdatedSteps = (
return step;
});
};
export const getGuideStatusOnStepCompletion = (
guideState: GuideState | undefined,
guideId: GuideId,
stepId: GuideStepIds
): GuideStatus => {
const stepConfig = getStepConfig(guideId, stepId);
const isManualCompletion = stepConfig?.manualCompletion || false;
const isLastStepInGuide = isLastStep(guideId, stepId);
const isCurrentStepReadyToComplete = isStepReadyToComplete(guideState, guideId, stepId);
// We want to set the guide status to 'ready_to_complete' if the current step is the last step in the guide
// and the step is not configured for manual completion
// or if the current step is configured for manual completion and the last step is ready to complete
if (
(isLastStepInGuide && !isManualCompletion) ||
(isLastStepInGuide && isManualCompletion && isCurrentStepReadyToComplete)
) {
return 'ready_to_complete';
}
// Otherwise the guide is still in progress
return 'in_progress';
};

View file

@ -6,12 +6,11 @@
* Side Public License, v 1.
*/
import React from 'react';
import { Observable } from 'rxjs';
import { HttpSetup } from '@kbn/core/public';
import type { GuideState, GuideId, GuideStepIds, StepStatus } from '@kbn/guided-onboarding';
import type { GuideState, GuideId, GuideStepIds } from '@kbn/guided-onboarding';
import type { CloudStart } from '@kbn/cloud-plugin/public';
import type { PluginStatus, PluginState } from '../common/types';
import type { PluginStatus, PluginState, GuideConfig } from '../common';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface GuidedOnboardingPluginSetup {}
@ -53,42 +52,5 @@ export interface GuidedOnboardingApi {
) => Promise<{ pluginState: PluginState } | undefined>;
skipGuidedOnboarding: () => Promise<{ pluginState: PluginState } | undefined>;
isGuidePanelOpen$: Observable<boolean>;
getGuideConfig: (guideId: GuideId) => Promise<GuideConfig | undefined>;
}
export interface StepConfig {
id: GuideStepIds;
title: string;
// description is displayed as a single paragraph, can be combined with description list
description?: string;
// description list is displayed as an unordered list, can be combined with description
descriptionList?: Array<string | React.ReactNode>;
location?: {
appID: string;
path: string;
};
status?: StepStatus;
integration?: string;
manualCompletion?: {
title: string;
description: string;
readyToCompleteOnNavigation?: boolean;
};
}
export interface GuideConfig {
title: string;
description: string;
guideName: string;
docs?: {
text: string;
url: string;
};
completedGuideRedirectLocation?: {
appID: string;
path: string;
};
steps: StepConfig[];
}
export type GuidesConfig = {
[key in GuideId]: GuideConfig;
};

View file

@ -6,11 +6,11 @@
* Side Public License, v 1.
*/
import type { GuidesConfig } from '../../types';
import type { GuidesConfig } from '../../../common';
import { testGuideConfig } from '../../../common';
import { securityConfig } from './security';
import { observabilityConfig } from './observability';
import { searchConfig } from './search';
import { testGuideConfig } from './test_guide';
export const guidesConfig: GuidesConfig = {
security: securityConfig,

View file

@ -6,13 +6,8 @@
* Side Public License, v 1.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiLink } from '@elastic/eui';
import type { GuideConfig } from '../../types';
import type { GuideConfig } from '../../../common';
export const observabilityConfig: GuideConfig = {
title: i18n.translate('guidedOnboarding.observabilityGuide.title', {
@ -36,21 +31,10 @@ export const observabilityConfig: GuideConfig = {
}),
integration: 'kubernetes',
descriptionList: [
<FormattedMessage
id="guidedOnboarding.observabilityGuide.addDataStep.descriptionList.item1"
defaultMessage="Deploy {kubeStateMetricsLink} service to your Kubernetes."
values={{
kubeStateMetricsLink: (
<EuiLink
external
target="_blank"
href="https://github.com/kubernetes/kube-state-metrics"
>
kube-state-metrics
</EuiLink>
),
}}
/>,
i18n.translate('guidedOnboarding.observabilityGuide.addDataStep.descriptionList.item1', {
// TODO add the link to the docs, when markdown support is implemented https://github.com/elastic/kibana/issues/146404
defaultMessage: 'Deploy kube-state-metrics service to your Kubernetes.',
}),
i18n.translate('guidedOnboarding.observabilityGuide.addDataStep.descriptionList.item2', {
defaultMessage: 'Add the Elastic Kubernetes integration.',
}),

View file

@ -7,7 +7,7 @@
*/
import { i18n } from '@kbn/i18n';
import type { GuideConfig } from '../../types';
import type { GuideConfig } from '../../../common';
export const searchConfig: GuideConfig = {
title: i18n.translate('guidedOnboarding.searchGuide.title', {

View file

@ -7,7 +7,7 @@
*/
import { i18n } from '@kbn/i18n';
import type { GuideConfig } from '../../types';
import type { GuideConfig } from '../../../common';
export const securityConfig: GuideConfig = {
title: i18n.translate('guidedOnboarding.securityGuide.title', {

View file

@ -8,7 +8,7 @@
import { SavedObjectsClient } from '@kbn/core/server';
import { findActiveGuide } from './guide_state_utils';
import { PluginState, PluginStatus } from '../../common/types';
import type { PluginState, PluginStatus } from '../../common';
import {
pluginStateSavedObjectsId,
pluginStateSavedObjectsType,

View 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 { IRouter } from '@kbn/core/server';
import { schema } from '@kbn/config-schema';
import type { GuideId } from '@kbn/guided-onboarding';
import { API_BASE_PATH } from '../../common';
import { guidesConfig } from '../helpers/guides_config';
export const registerGetConfigRoute = (router: IRouter) => {
// Fetch the config of the guide
router.get(
{
path: `${API_BASE_PATH}/configs/{guideId}`,
validate: {
params: schema.object({
guideId: schema.string(),
}),
},
},
async (context, request, response) => {
const { guideId } = request.params;
if (guidesConfig && guideId && Object.keys(guidesConfig).includes(guideId)) {
return response.ok({
body: { config: guidesConfig[guideId as GuideId] },
});
}
return response.notFound();
}
);
};

View file

@ -7,7 +7,7 @@
*/
import { IRouter, SavedObjectsClient } from '@kbn/core/server';
import { API_BASE_PATH } from '../../common/constants';
import { API_BASE_PATH } from '../../common';
import { findAllGuides } from '../helpers';
export const registerGetGuideStateRoute = (router: IRouter) => {

View file

@ -9,10 +9,13 @@
import type { IRouter } from '@kbn/core/server';
import { registerGetGuideStateRoute } from './guide_state_routes';
import { registerGetPluginStateRoute, registerPutPluginStateRoute } from './plugin_state_routes';
import { registerGetConfigRoute } from './config_routes';
export function defineRoutes(router: IRouter) {
registerGetGuideStateRoute(router);
registerGetPluginStateRoute(router);
registerPutPluginStateRoute(router);
registerGetConfigRoute(router);
}

View file

@ -10,7 +10,7 @@ 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 { API_BASE_PATH } from '../../common';
import { updateGuideState } from '../helpers';
export const registerGetPluginStateRoute = (router: IRouter) => {

View file

@ -0,0 +1,27 @@
/*
* 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 type { FtrProviderContext } from '../../ftr_provider_context';
const getConfigsPath = '/api/guided_onboarding/configs';
export default function testGetGuidesState({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
describe('GET /api/guided_onboarding/configs', () => {
// check that all guides are present
['testGuide', 'security', 'search', 'observability'].map((guideId) => {
it(`returns config for ${guideId}`, async () => {
const response = await supertest.get(`${getConfigsPath}/${guideId}`).expect(200);
expect(response.body).not.to.be.empty();
const { config } = response.body;
expect(config).to.not.be.empty();
});
});
});
}

View file

@ -13,5 +13,6 @@ export default function apiIntegrationTests({ loadTestFile }: FtrProviderContext
loadTestFile(require.resolve('./get_state'));
loadTestFile(require.resolve('./put_state'));
loadTestFile(require.resolve('./get_guides'));
loadTestFile(require.resolve('./get_config'));
});
}