mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
9e6f24a331
commit
e7da574c5d
29 changed files with 723 additions and 206 deletions
|
@ -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>
|
||||
|
|
11
src/plugins/guided_onboarding/common/index.ts
Normal file
11
src/plugins/guided_onboarding/common/index.ts
Normal 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';
|
|
@ -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',
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -16,5 +16,3 @@ export type {
|
|||
GuidedOnboardingPluginStart,
|
||||
GuidedOnboardingApi,
|
||||
} from './types';
|
||||
|
||||
export { guidesConfig } from './constants/guides_config';
|
||||
|
|
|
@ -25,6 +25,7 @@ const apiServiceMock: jest.Mocked<GuidedOnboardingPluginStart> = {
|
|||
completeGuidedOnboardingForIntegration: jest.fn(),
|
||||
skipGuidedOnboarding: jest.fn(),
|
||||
isGuidePanelOpen$: new BehaviorSubject(false),
|
||||
getGuideConfig: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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,
|
|
@ -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.',
|
||||
}),
|
|
@ -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', {
|
|
@ -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', {
|
|
@ -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,
|
||||
|
|
36
src/plugins/guided_onboarding/server/routes/config_routes.ts
Normal file
36
src/plugins/guided_onboarding/server/routes/config_routes.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 { 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();
|
||||
}
|
||||
);
|
||||
};
|
|
@ -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) => {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
27
test/api_integration/apis/guided_onboarding/get_config.ts
Normal file
27
test/api_integration/apis/guided_onboarding/get_config.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue