mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Guided onboarding] Added Guided onboarding to the Fleet plugin (#142185)
* [Guided onboarding] Added a EuiTour for guided onboarding in Integrations * [Guided onboarding] Added data step completion for Elastic Defend * [Guided onboarding] Added tests for api * [Guided onboarding] Fixed jest tests * [Guided onboarding] Added a fail safe for guided onboarding plugin not available in fleet * [Guided onboarding] Moved the guided onboarding hook to a different folder in the fleet plugin, also fixed tests * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * [Guided onboarding] Updates after the merge conflicts * [Guided onboarding] Fixed typos * [Guided onboarding] Fixed types error * Update x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/with_guided_onboarding_tour.tsx Co-authored-by: Kelly Murphy <kelly.murphy@elastic.co> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kelly Murphy <kelly.murphy@elastic.co>
This commit is contained in:
parent
0a03a527e9
commit
27ac4fc26e
20 changed files with 532 additions and 79 deletions
|
@ -21,6 +21,11 @@ export const securityConfig: GuideConfig = {
|
|||
'Nullam ligula enim, malesuada a finibus vel, cursus sed risus.',
|
||||
'Vivamus pretium, elit dictum lacinia aliquet, libero nibh dictum enim, a rhoncus leo magna in sapien.',
|
||||
],
|
||||
integration: 'endpoint',
|
||||
location: {
|
||||
appID: 'integrations',
|
||||
path: '/browse/security',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'rules',
|
||||
|
|
31
src/plugins/guided_onboarding/public/mocks.tsx
Normal file
31
src/plugins/guided_onboarding/public/mocks.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { BehaviorSubject } from 'rxjs';
|
||||
import { GuidedOnboardingPluginStart } from '.';
|
||||
|
||||
const apiServiceMock: jest.Mocked<GuidedOnboardingPluginStart> = {
|
||||
guidedOnboardingApi: {
|
||||
setup: jest.fn(),
|
||||
fetchActiveGuideState$: () => new BehaviorSubject(undefined),
|
||||
fetchAllGuidesState: jest.fn(),
|
||||
updateGuideState: jest.fn(),
|
||||
activateGuide: jest.fn(),
|
||||
completeGuide: jest.fn(),
|
||||
isGuideStepActive$: () => new BehaviorSubject(false),
|
||||
startGuideStep: jest.fn(),
|
||||
completeGuideStep: jest.fn(),
|
||||
isGuidedOnboardingActiveForIntegration$: () => new BehaviorSubject(false),
|
||||
completeGuidedOnboardingForIntegration: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
export const guidedOnboardingMock = {
|
||||
createSetup: () => {},
|
||||
createStart: () => apiServiceMock,
|
||||
};
|
97
src/plugins/guided_onboarding/public/services/api.mocks.ts
Normal file
97
src/plugins/guided_onboarding/public/services/api.mocks.ts
Normal file
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { GuideState } from '../../common/types';
|
||||
|
||||
export const searchAddDataActiveState: GuideState = {
|
||||
guideId: 'search',
|
||||
isActive: true,
|
||||
status: 'in_progress',
|
||||
steps: [
|
||||
{
|
||||
id: 'add_data',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: 'browse_docs',
|
||||
status: 'inactive',
|
||||
},
|
||||
{
|
||||
id: 'search_experience',
|
||||
status: 'inactive',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const searchAddDataInProgressState: GuideState = {
|
||||
isActive: true,
|
||||
status: 'in_progress',
|
||||
steps: [
|
||||
{
|
||||
id: 'add_data',
|
||||
status: 'in_progress',
|
||||
},
|
||||
{
|
||||
id: 'browse_docs',
|
||||
status: 'inactive',
|
||||
},
|
||||
{
|
||||
id: 'search_experience',
|
||||
status: 'inactive',
|
||||
},
|
||||
],
|
||||
guideId: 'search',
|
||||
};
|
||||
|
||||
export const securityAddDataInProgressState: GuideState = {
|
||||
guideId: 'security',
|
||||
status: 'in_progress',
|
||||
isActive: true,
|
||||
steps: [
|
||||
{
|
||||
id: 'add_data',
|
||||
status: 'in_progress',
|
||||
},
|
||||
{
|
||||
id: 'rules',
|
||||
status: 'inactive',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const securityRulesActivesState: GuideState = {
|
||||
guideId: 'security',
|
||||
isActive: true,
|
||||
status: 'in_progress',
|
||||
steps: [
|
||||
{
|
||||
id: 'add_data',
|
||||
status: 'complete',
|
||||
},
|
||||
{
|
||||
id: 'rules',
|
||||
status: 'active',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const noGuideActiveState: GuideState = {
|
||||
guideId: 'security',
|
||||
status: 'in_progress',
|
||||
isActive: false,
|
||||
steps: [
|
||||
{
|
||||
id: 'add_data',
|
||||
status: 'in_progress',
|
||||
},
|
||||
{
|
||||
id: 'rules',
|
||||
status: 'inactive',
|
||||
},
|
||||
],
|
||||
};
|
|
@ -14,29 +14,17 @@ import { API_BASE_PATH } from '../../common/constants';
|
|||
import { guidesConfig } from '../constants/guides_config';
|
||||
import type { GuideState } from '../../common/types';
|
||||
import { ApiService } from './api';
|
||||
import {
|
||||
noGuideActiveState,
|
||||
searchAddDataActiveState,
|
||||
securityAddDataInProgressState,
|
||||
securityRulesActivesState,
|
||||
} from './api.mocks';
|
||||
|
||||
const searchGuide = 'search';
|
||||
const firstStep = guidesConfig[searchGuide].steps[0].id;
|
||||
|
||||
const mockActiveSearchGuideState: GuideState = {
|
||||
guideId: searchGuide,
|
||||
isActive: true,
|
||||
status: 'in_progress',
|
||||
steps: [
|
||||
{
|
||||
id: 'add_data',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: 'browse_docs',
|
||||
status: 'inactive',
|
||||
},
|
||||
{
|
||||
id: 'search_experience',
|
||||
status: 'inactive',
|
||||
},
|
||||
],
|
||||
};
|
||||
const endpointIntegration = 'endpoint';
|
||||
const kubernetesIntegration = 'kubernetes';
|
||||
|
||||
describe('GuidedOnboarding ApiService', () => {
|
||||
let httpClient: jest.Mocked<HttpSetup>;
|
||||
|
@ -46,7 +34,7 @@ describe('GuidedOnboarding ApiService', () => {
|
|||
beforeEach(() => {
|
||||
httpClient = httpServiceMock.createStartContract({ basePath: '/base/path' });
|
||||
httpClient.get.mockResolvedValue({
|
||||
state: { activeGuide: searchGuide, activeStep: firstStep },
|
||||
state: [securityAddDataInProgressState],
|
||||
});
|
||||
apiService = new ApiService();
|
||||
apiService.setup(httpClient);
|
||||
|
@ -72,7 +60,7 @@ describe('GuidedOnboarding ApiService', () => {
|
|||
await apiService.activateGuide(searchGuide);
|
||||
|
||||
const state = await firstValueFrom(apiService.fetchActiveGuideState$());
|
||||
expect(state).toEqual(mockActiveSearchGuideState);
|
||||
expect(state).toEqual(searchAddDataActiveState);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -87,14 +75,14 @@ describe('GuidedOnboarding ApiService', () => {
|
|||
describe('updateGuideState', () => {
|
||||
it('sends a request to the put API', async () => {
|
||||
const updatedState: GuideState = {
|
||||
...mockActiveSearchGuideState,
|
||||
...searchAddDataActiveState,
|
||||
steps: [
|
||||
{
|
||||
id: mockActiveSearchGuideState.steps[0].id,
|
||||
id: searchAddDataActiveState.steps[0].id,
|
||||
status: 'in_progress', // update the first step status
|
||||
},
|
||||
mockActiveSearchGuideState.steps[1],
|
||||
mockActiveSearchGuideState.steps[2],
|
||||
searchAddDataActiveState.steps[1],
|
||||
searchAddDataActiveState.steps[2],
|
||||
],
|
||||
};
|
||||
await apiService.updateGuideState(updatedState, false);
|
||||
|
@ -108,14 +96,14 @@ describe('GuidedOnboarding ApiService', () => {
|
|||
describe('isGuideStepActive$', () => {
|
||||
it('returns true if the step has been started', async (done) => {
|
||||
const updatedState: GuideState = {
|
||||
...mockActiveSearchGuideState,
|
||||
...searchAddDataActiveState,
|
||||
steps: [
|
||||
{
|
||||
id: mockActiveSearchGuideState.steps[0].id,
|
||||
id: searchAddDataActiveState.steps[0].id,
|
||||
status: 'in_progress',
|
||||
},
|
||||
mockActiveSearchGuideState.steps[1],
|
||||
mockActiveSearchGuideState.steps[2],
|
||||
searchAddDataActiveState.steps[1],
|
||||
searchAddDataActiveState.steps[2],
|
||||
],
|
||||
};
|
||||
await apiService.updateGuideState(updatedState, false);
|
||||
|
@ -130,7 +118,7 @@ describe('GuidedOnboarding ApiService', () => {
|
|||
});
|
||||
|
||||
it('returns false if the step is not been started', async (done) => {
|
||||
await apiService.updateGuideState(mockActiveSearchGuideState, false);
|
||||
await apiService.updateGuideState(searchAddDataActiveState, false);
|
||||
subscription = apiService
|
||||
.isGuideStepActive$(searchGuide, firstStep)
|
||||
.subscribe((isStepActive) => {
|
||||
|
@ -170,21 +158,18 @@ describe('GuidedOnboarding ApiService', () => {
|
|||
});
|
||||
|
||||
it('reactivates a guide that has already been started', async () => {
|
||||
await apiService.activateGuide(searchGuide, mockActiveSearchGuideState);
|
||||
await apiService.activateGuide(searchGuide, searchAddDataActiveState);
|
||||
|
||||
expect(httpClient.put).toHaveBeenCalledTimes(1);
|
||||
expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
|
||||
body: JSON.stringify({
|
||||
...mockActiveSearchGuideState,
|
||||
isActive: true,
|
||||
}),
|
||||
body: JSON.stringify(searchAddDataActiveState),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('completeGuide', () => {
|
||||
const readyToCompleteGuideState: GuideState = {
|
||||
...mockActiveSearchGuideState,
|
||||
...searchAddDataActiveState,
|
||||
steps: [
|
||||
{
|
||||
id: 'add_data',
|
||||
|
@ -224,7 +209,7 @@ describe('GuidedOnboarding ApiService', () => {
|
|||
|
||||
it('returns undefined if the selected guide has uncompleted steps', async () => {
|
||||
const incompleteGuideState: GuideState = {
|
||||
...mockActiveSearchGuideState,
|
||||
...searchAddDataActiveState,
|
||||
steps: [
|
||||
{
|
||||
id: 'add_data',
|
||||
|
@ -249,7 +234,7 @@ describe('GuidedOnboarding ApiService', () => {
|
|||
|
||||
describe('startGuideStep', () => {
|
||||
beforeEach(async () => {
|
||||
await apiService.updateGuideState(mockActiveSearchGuideState, false);
|
||||
await apiService.updateGuideState(searchAddDataActiveState, false);
|
||||
});
|
||||
|
||||
it('updates the selected step and marks it as in_progress', async () => {
|
||||
|
@ -257,16 +242,16 @@ describe('GuidedOnboarding ApiService', () => {
|
|||
|
||||
expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
|
||||
body: JSON.stringify({
|
||||
...mockActiveSearchGuideState,
|
||||
...searchAddDataActiveState,
|
||||
isActive: true,
|
||||
status: 'in_progress',
|
||||
steps: [
|
||||
{
|
||||
id: mockActiveSearchGuideState.steps[0].id,
|
||||
id: searchAddDataActiveState.steps[0].id,
|
||||
status: 'in_progress',
|
||||
},
|
||||
mockActiveSearchGuideState.steps[1],
|
||||
mockActiveSearchGuideState.steps[2],
|
||||
searchAddDataActiveState.steps[1],
|
||||
searchAddDataActiveState.steps[2],
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
@ -281,14 +266,14 @@ describe('GuidedOnboarding ApiService', () => {
|
|||
describe('completeGuideStep', () => {
|
||||
it(`completes the step when it's in progress`, async () => {
|
||||
const updatedState: GuideState = {
|
||||
...mockActiveSearchGuideState,
|
||||
...searchAddDataActiveState,
|
||||
steps: [
|
||||
{
|
||||
id: mockActiveSearchGuideState.steps[0].id,
|
||||
id: searchAddDataActiveState.steps[0].id,
|
||||
status: 'in_progress', // Mark a step as in_progress in order to test the "completeGuideStep" behavior
|
||||
},
|
||||
mockActiveSearchGuideState.steps[1],
|
||||
mockActiveSearchGuideState.steps[2],
|
||||
searchAddDataActiveState.steps[1],
|
||||
searchAddDataActiveState.steps[2],
|
||||
],
|
||||
};
|
||||
await apiService.updateGuideState(updatedState, false);
|
||||
|
@ -303,14 +288,14 @@ describe('GuidedOnboarding ApiService', () => {
|
|||
...updatedState,
|
||||
steps: [
|
||||
{
|
||||
id: mockActiveSearchGuideState.steps[0].id,
|
||||
id: searchAddDataActiveState.steps[0].id,
|
||||
status: 'complete',
|
||||
},
|
||||
{
|
||||
id: mockActiveSearchGuideState.steps[1].id,
|
||||
id: searchAddDataActiveState.steps[1].id,
|
||||
status: 'active',
|
||||
},
|
||||
mockActiveSearchGuideState.steps[2],
|
||||
searchAddDataActiveState.steps[2],
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
@ -322,11 +307,91 @@ describe('GuidedOnboarding ApiService', () => {
|
|||
});
|
||||
|
||||
it('does nothing if the step is not in progress', async () => {
|
||||
await apiService.updateGuideState(mockActiveSearchGuideState, false);
|
||||
await apiService.updateGuideState(searchAddDataActiveState, false);
|
||||
|
||||
await apiService.completeGuideStep(searchGuide, firstStep);
|
||||
// Expect only 1 call from updateGuideState()
|
||||
expect(httpClient.put).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isGuidedOnboardingActiveForIntegration$', () => {
|
||||
it('returns true if the integration is part of the active step', async (done) => {
|
||||
httpClient.get.mockResolvedValue({
|
||||
state: [securityAddDataInProgressState],
|
||||
});
|
||||
apiService.setup(httpClient);
|
||||
subscription = apiService
|
||||
.isGuidedOnboardingActiveForIntegration$(endpointIntegration)
|
||||
.subscribe((isIntegrationInGuideStep) => {
|
||||
if (isIntegrationInGuideStep) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('returns false if another integration is part of the active step', async (done) => {
|
||||
httpClient.get.mockResolvedValue({
|
||||
state: [securityAddDataInProgressState],
|
||||
});
|
||||
apiService.setup(httpClient);
|
||||
subscription = apiService
|
||||
.isGuidedOnboardingActiveForIntegration$(kubernetesIntegration)
|
||||
.subscribe((isIntegrationInGuideStep) => {
|
||||
if (!isIntegrationInGuideStep) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('returns false if no guide is active', async (done) => {
|
||||
httpClient.get.mockResolvedValue({
|
||||
state: [noGuideActiveState],
|
||||
});
|
||||
apiService.setup(httpClient);
|
||||
subscription = apiService
|
||||
.isGuidedOnboardingActiveForIntegration$(endpointIntegration)
|
||||
.subscribe((isIntegrationInGuideStep) => {
|
||||
if (!isIntegrationInGuideStep) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('completeGuidedOnboardingForIntegration', () => {
|
||||
it(`completes the step if it's active for the integration`, async () => {
|
||||
httpClient.get.mockResolvedValue({
|
||||
state: [securityAddDataInProgressState],
|
||||
});
|
||||
apiService.setup(httpClient);
|
||||
|
||||
await apiService.completeGuidedOnboardingForIntegration(endpointIntegration);
|
||||
expect(httpClient.put).toHaveBeenCalledTimes(1);
|
||||
// this assertion depends on the guides config
|
||||
expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, {
|
||||
body: JSON.stringify(securityRulesActivesState),
|
||||
});
|
||||
});
|
||||
|
||||
it(`does nothing if the step has a different integration`, async () => {
|
||||
httpClient.get.mockResolvedValue({
|
||||
state: [securityAddDataInProgressState],
|
||||
});
|
||||
apiService.setup(httpClient);
|
||||
|
||||
await apiService.completeGuidedOnboardingForIntegration(kubernetesIntegration);
|
||||
expect(httpClient.put).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it(`does nothing if no guide is active`, async () => {
|
||||
httpClient.get.mockResolvedValue({
|
||||
state: [noGuideActiveState],
|
||||
});
|
||||
apiService.setup(httpClient);
|
||||
|
||||
await apiService.completeGuidedOnboardingForIntegration(endpointIntegration);
|
||||
expect(httpClient.put).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,11 +9,17 @@
|
|||
import { HttpSetup } from '@kbn/core/public';
|
||||
import { BehaviorSubject, map, from, concatMap, of, Observable, firstValueFrom } from 'rxjs';
|
||||
|
||||
import { GuidedOnboardingApi } from '../types';
|
||||
import {
|
||||
getGuideConfig,
|
||||
getInProgressStepId,
|
||||
isIntegrationInGuideStep,
|
||||
isLastStep,
|
||||
} from './helpers';
|
||||
import { API_BASE_PATH } from '../../common/constants';
|
||||
import type { GuideState, GuideId, GuideStep, GuideStepIds } from '../../common/types';
|
||||
import { isLastStep, getGuideConfig } from './helpers';
|
||||
|
||||
export class ApiService {
|
||||
export class ApiService implements GuidedOnboardingApi {
|
||||
private client: HttpSetup | undefined;
|
||||
private onboardingGuideState$!: BehaviorSubject<GuideState | undefined>;
|
||||
public isGuidePanelOpen$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
|
@ -102,8 +108,8 @@ export class ApiService {
|
|||
/**
|
||||
* Activates a guide by guideId
|
||||
* This is useful for the onboarding landing page, when a user selects a guide to start or continue
|
||||
* @param {GuideId} guideID the id of the guide (one of search, observability, security)
|
||||
* @param {GuideState} guideState (optional) the selected guide state, if it exists (i.e., if a user is continuing a guide)
|
||||
* @param {GuideId} guideId the id of the guide (one of search, observability, security)
|
||||
* @param {GuideState} guide (optional) the selected guide state, if it exists (i.e., if a user is continuing a guide)
|
||||
* @return {Promise} a promise with the updated guide state
|
||||
*/
|
||||
public async activateGuide(
|
||||
|
@ -150,7 +156,7 @@ export class ApiService {
|
|||
* Completes a guide
|
||||
* Updates the overall guide status to 'complete', and marks it as inactive
|
||||
* This is useful for the dropdown panel, when the user clicks the "Continue using Elastic" button after completing all steps
|
||||
* @param {GuideId} guideID the id of the guide (one of search, observability, security)
|
||||
* @param {GuideId} guideId the id of the guide (one of search, observability, security)
|
||||
* @return {Promise} a promise with the updated guide state
|
||||
*/
|
||||
public async completeGuide(guideId: GuideId): Promise<{ state: GuideState } | undefined> {
|
||||
|
@ -300,6 +306,38 @@ export class ApiService {
|
|||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* An observable with the boolean value if the guided onboarding is currently active for the integration.
|
||||
* Returns true, if the passed integration is used in the current guide's step.
|
||||
* Returns false otherwise.
|
||||
* @param {string} integration the integration (package name) to check for in the guided onboarding config
|
||||
* @return {Observable} an observable with the boolean value
|
||||
*/
|
||||
public isGuidedOnboardingActiveForIntegration$(integration?: string): Observable<boolean> {
|
||||
return this.fetchActiveGuideState$().pipe(
|
||||
map((state) => {
|
||||
return state ? isIntegrationInGuideStep(state, integration) : false;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public async completeGuidedOnboardingForIntegration(
|
||||
integration?: string
|
||||
): Promise<{ state: GuideState } | undefined> {
|
||||
if (integration) {
|
||||
const currentState = await firstValueFrom(this.fetchActiveGuideState$());
|
||||
if (currentState) {
|
||||
const inProgressStepId = getInProgressStepId(currentState);
|
||||
if (inProgressStepId) {
|
||||
const isIntegrationStepActive = isIntegrationInGuideStep(currentState, integration);
|
||||
if (isIntegrationStepActive) {
|
||||
return await this.completeGuideStep(currentState?.guideId, inProgressStepId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const apiService = new ApiService();
|
||||
|
|
|
@ -7,11 +7,16 @@
|
|||
*/
|
||||
|
||||
import { guidesConfig } from '../constants/guides_config';
|
||||
import { isLastStep } from './helpers';
|
||||
import { isIntegrationInGuideStep, isLastStep } from './helpers';
|
||||
import {
|
||||
noGuideActiveState,
|
||||
securityAddDataInProgressState,
|
||||
securityRulesActivesState,
|
||||
} from './api.mocks';
|
||||
|
||||
const searchGuide = 'search';
|
||||
const firstStep = guidesConfig[searchGuide].steps[0].id;
|
||||
const lastStep = guidesConfig[searchGuide].steps[2].id;
|
||||
const lastStep = guidesConfig[searchGuide].steps[guidesConfig[searchGuide].steps.length - 1].id;
|
||||
|
||||
describe('GuidedOnboarding ApiService helpers', () => {
|
||||
// this test suite depends on the guides config
|
||||
|
@ -26,4 +31,27 @@ describe('GuidedOnboarding ApiService helpers', () => {
|
|||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isIntegrationInGuideStep', () => {
|
||||
it('return true if the integration is defined in the guide step config', () => {
|
||||
const result = isIntegrationInGuideStep(securityAddDataInProgressState, 'endpoint');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
it('returns false if a different integration is defined in the guide step', () => {
|
||||
const result = isIntegrationInGuideStep(securityAddDataInProgressState, 'kubernetes');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
it('returns false if no integration is defined in the guide step', () => {
|
||||
const result = isIntegrationInGuideStep(securityRulesActivesState, 'endpoint');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
it('returns false if no guide is active', () => {
|
||||
const result = isIntegrationInGuideStep(noGuideActiveState, 'endpoint');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
it('returns false if no integration passed', () => {
|
||||
const result = isIntegrationInGuideStep(securityAddDataInProgressState);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { GuideId } from '../../common/types';
|
||||
import type { GuideId, GuideState, GuideStepIds } from '../../common/types';
|
||||
import { guidesConfig } from '../constants/guides_config';
|
||||
import type { GuideConfig, StepConfig } from '../types';
|
||||
import { GuideConfig, StepConfig } from '../types';
|
||||
|
||||
export const getGuideConfig = (guideID?: string): GuideConfig | undefined => {
|
||||
if (guideID && Object.keys(guidesConfig).includes(guideID)) {
|
||||
|
@ -33,3 +33,26 @@ export const isLastStep = (guideID: string, stepID: string): boolean => {
|
|||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const getInProgressStepId = (state: GuideState): GuideStepIds | undefined => {
|
||||
const inProgressStep = state.steps.find((step) => step.status === 'in_progress');
|
||||
return inProgressStep ? inProgressStep.id : undefined;
|
||||
};
|
||||
|
||||
const getInProgressStepConfig = (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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const isIntegrationInGuideStep = (state: GuideState, integration?: string): boolean => {
|
||||
if (state.isActive) {
|
||||
const stepConfig = getInProgressStepConfig(state);
|
||||
return stepConfig ? stepConfig.integration === integration : false;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
|
|
@ -6,15 +6,16 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public';
|
||||
import { GuideId, GuideStepIds, StepStatus } from '../common/types';
|
||||
import { ApiService } from './services/api';
|
||||
import { HttpSetup } from '@kbn/core/public';
|
||||
import { GuideId, GuideState, GuideStepIds, StepStatus } from '../common/types';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface GuidedOnboardingPluginSetup {}
|
||||
|
||||
export interface GuidedOnboardingPluginStart {
|
||||
guidedOnboardingApi?: ApiService;
|
||||
guidedOnboardingApi?: GuidedOnboardingApi;
|
||||
}
|
||||
|
||||
export interface AppPluginStartDependencies {
|
||||
|
@ -25,6 +26,34 @@ export interface ClientConfigType {
|
|||
ui: boolean;
|
||||
}
|
||||
|
||||
export interface GuidedOnboardingApi {
|
||||
setup: (httpClient: HttpSetup) => void;
|
||||
fetchActiveGuideState$: () => Observable<GuideState | undefined>;
|
||||
fetchAllGuidesState: () => Promise<{ state: GuideState[] } | undefined>;
|
||||
updateGuideState: (
|
||||
newState: GuideState,
|
||||
panelState: boolean
|
||||
) => Promise<{ state: GuideState } | undefined>;
|
||||
activateGuide: (
|
||||
guideId: GuideId,
|
||||
guide?: GuideState
|
||||
) => Promise<{ state: GuideState } | undefined>;
|
||||
completeGuide: (guideId: GuideId) => Promise<{ state: GuideState } | undefined>;
|
||||
isGuideStepActive$: (guideId: GuideId, stepId: GuideStepIds) => Observable<boolean>;
|
||||
startGuideStep: (
|
||||
guideId: GuideId,
|
||||
stepId: GuideStepIds
|
||||
) => Promise<{ state: GuideState } | undefined>;
|
||||
completeGuideStep: (
|
||||
guideId: GuideId,
|
||||
stepId: GuideStepIds
|
||||
) => Promise<{ state: GuideState } | undefined>;
|
||||
isGuidedOnboardingActiveForIntegration$: (integration?: string) => Observable<boolean>;
|
||||
completeGuidedOnboardingForIntegration: (
|
||||
integration?: string
|
||||
) => Promise<{ state: GuideState } | undefined>;
|
||||
}
|
||||
|
||||
export interface StepConfig {
|
||||
id: GuideStepIds;
|
||||
title: string;
|
||||
|
@ -34,6 +63,7 @@ export interface StepConfig {
|
|||
path: string;
|
||||
};
|
||||
status?: StepStatus;
|
||||
integration?: string;
|
||||
}
|
||||
export interface GuideConfig {
|
||||
title: string;
|
||||
|
|
|
@ -15,6 +15,7 @@ import { I18nProvider } from '@kbn/i18n-react';
|
|||
|
||||
import { CoreScopedHistory } from '@kbn/core/public';
|
||||
import { getStorybookContextProvider } from '@kbn/custom-integrations-plugin/storybook';
|
||||
import { guidedOnboardingMock } from '@kbn/guided-onboarding-plugin/public/mocks';
|
||||
|
||||
import { IntegrationsAppContext } from '../../public/applications/integrations/app';
|
||||
import type { FleetConfigType, FleetStartServices } from '../../public/plugin';
|
||||
|
@ -110,6 +111,7 @@ export const StorybookContext: React.FC<{ storyContext?: Parameters<DecoratorFn>
|
|||
writeIntegrationPolicies: true,
|
||||
},
|
||||
},
|
||||
guidedOnboarding: guidedOnboardingMock.createStart(),
|
||||
}),
|
||||
[isCloudEnabled]
|
||||
);
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
"server": true,
|
||||
"ui": true,
|
||||
"configPath": ["xpack", "fleet"],
|
||||
"requiredPlugins": ["licensing", "data", "encryptedSavedObjects", "navigation", "customIntegrations", "share", "spaces", "security", "unifiedSearch", "savedObjectsTagging", "taskManager"],
|
||||
"requiredPlugins": ["licensing", "data", "encryptedSavedObjects", "navigation", "customIntegrations", "share", "spaces", "security", "unifiedSearch", "savedObjectsTagging", "taskManager", "guidedOnboarding"],
|
||||
"optionalPlugins": ["features", "cloud", "usageCollection", "home", "globalSearch", "telemetry", "discover", "ingestPipelines"],
|
||||
"extraPublicDirs": ["common"],
|
||||
"requiredBundles": ["kibanaReact", "cloudChat", "esUiShared", "infra", "kibanaUtils", "usageCollection", "unifiedSearch"]
|
||||
|
|
|
@ -27,6 +27,8 @@ import type { SearchHit } from '@kbn/es-types';
|
|||
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { useStartServices, useIsGuidedOnboardingActive } from '../../../../../../../hooks';
|
||||
|
||||
import type { PackageInfo } from '../../../../../../../../common';
|
||||
|
||||
import {
|
||||
|
@ -136,8 +138,15 @@ export const ConfirmIncomingDataWithPreview: React.FunctionComponent<Props> = ({
|
|||
);
|
||||
const { enrolledAgents, numAgentsWithData } = useGetAgentIncomingData(incomingData, packageInfo);
|
||||
|
||||
const isGuidedOnboardingActive = useIsGuidedOnboardingActive(packageInfo?.name);
|
||||
const { guidedOnboarding } = useStartServices();
|
||||
if (!isLoading && enrolledAgents > 0 && numAgentsWithData > 0) {
|
||||
setAgentDataConfirmed(true);
|
||||
if (isGuidedOnboardingActive) {
|
||||
guidedOnboarding.guidedOnboardingApi?.completeGuidedOnboardingForIntegration(
|
||||
packageInfo?.name
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!agentDataConfirmed) {
|
||||
return (
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import type { FunctionComponent, ReactElement } from 'react';
|
||||
import { EuiButton, EuiText, EuiTourStep } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
type TourType = 'addIntegrationButton' | 'integrationsList';
|
||||
const getTourConfig = (packageKey: string, tourType: TourType) => {
|
||||
if (packageKey.startsWith('endpoint') && tourType === 'addIntegrationButton') {
|
||||
return {
|
||||
title: i18n.translate('xpack.fleet.guidedOnboardingTour.endpointButton.title', {
|
||||
defaultMessage: 'Add Elastic Defend',
|
||||
}),
|
||||
description: i18n.translate('xpack.fleet.guidedOnboardingTour.endpointButton.description', {
|
||||
defaultMessage:
|
||||
'In just a few steps, configure your data with our recommended defaults. You can change this later.',
|
||||
}),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
export const WithGuidedOnboardingTour: FunctionComponent<{
|
||||
packageKey: string;
|
||||
isGuidedOnboardingActive: boolean;
|
||||
tourType: TourType;
|
||||
children: ReactElement;
|
||||
}> = ({ packageKey, isGuidedOnboardingActive, tourType, children }) => {
|
||||
const [isGuidedOnboardingTourOpen, setIsGuidedOnboardingTourOpen] =
|
||||
useState<boolean>(isGuidedOnboardingActive);
|
||||
useEffect(() => {
|
||||
setIsGuidedOnboardingTourOpen(isGuidedOnboardingActive);
|
||||
}, [isGuidedOnboardingActive]);
|
||||
const config = getTourConfig(packageKey, tourType);
|
||||
|
||||
return config ? (
|
||||
<EuiTourStep
|
||||
content={<EuiText>{config.description}</EuiText>}
|
||||
isStepOpen={isGuidedOnboardingTourOpen}
|
||||
maxWidth={350}
|
||||
onFinish={() => setIsGuidedOnboardingTourOpen(false)}
|
||||
step={1}
|
||||
stepsTotal={1}
|
||||
title={config.title}
|
||||
anchorPosition="rightUp"
|
||||
footerAction={
|
||||
<EuiButton onClick={() => setIsGuidedOnboardingTourOpen(false)} size="s" color="success">
|
||||
{i18n.translate('xpack.fleet.guidedOnboardingTour.nextButtonLabel', {
|
||||
defaultMessage: 'Next',
|
||||
})}
|
||||
</EuiButton>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</EuiTourStep>
|
||||
) : (
|
||||
<>{children}</>
|
||||
);
|
||||
};
|
|
@ -39,7 +39,12 @@ import {
|
|||
} from '../../../../hooks';
|
||||
import { INTEGRATIONS_ROUTING_PATHS } from '../../../../constants';
|
||||
import { ExperimentalFeaturesService } from '../../../../services';
|
||||
import { useGetPackageInfoByKey, useLink, useAgentPolicyContext } from '../../../../hooks';
|
||||
import {
|
||||
useGetPackageInfoByKey,
|
||||
useLink,
|
||||
useAgentPolicyContext,
|
||||
useIsGuidedOnboardingActive,
|
||||
} from '../../../../hooks';
|
||||
import { pkgKeyFromPackageInfo } from '../../../../services';
|
||||
import type { DetailViewPanelName, PackageInfo } from '../../../../types';
|
||||
import { InstallStatus } from '../../../../types';
|
||||
|
@ -47,6 +52,8 @@ import { Error, Loading, HeaderReleaseBadge } from '../../../../components';
|
|||
import type { WithHeaderLayoutProps } from '../../../../layouts';
|
||||
import { WithHeaderLayout } from '../../../../layouts';
|
||||
|
||||
import { WithGuidedOnboardingTour } from './components/with_guided_onboarding_tour';
|
||||
|
||||
import { useIsFirstTimeAgentUser } from './hooks';
|
||||
import { getInstallPkgRouteOptions } from './utils';
|
||||
import {
|
||||
|
@ -154,6 +161,7 @@ export function Detail() {
|
|||
|
||||
const { isFirstTimeAgentUser = false, isLoading: firstTimeUserLoading } =
|
||||
useIsFirstTimeAgentUser();
|
||||
const isGuidedOnboardingActive = useIsGuidedOnboardingActive(pkgName);
|
||||
|
||||
// Refresh package info when status change
|
||||
const [oldPackageInstallStatus, setOldPackageStatus] = useState(packageInstallStatus);
|
||||
|
@ -292,6 +300,7 @@ export function Detail() {
|
|||
isCloud,
|
||||
isExperimentalAddIntegrationPageEnabled,
|
||||
isFirstTimeAgentUser,
|
||||
isGuidedOnboardingActive,
|
||||
pkgkey,
|
||||
});
|
||||
|
||||
|
@ -305,6 +314,7 @@ export function Detail() {
|
|||
isCloud,
|
||||
isExperimentalAddIntegrationPageEnabled,
|
||||
isFirstTimeAgentUser,
|
||||
isGuidedOnboardingActive,
|
||||
pathname,
|
||||
pkgkey,
|
||||
search,
|
||||
|
@ -349,19 +359,25 @@ export function Detail() {
|
|||
{ isDivider: true },
|
||||
{
|
||||
content: (
|
||||
<AddIntegrationButton
|
||||
userCanInstallPackages={userCanInstallPackages}
|
||||
href={getHref('add_integration_to_policy', {
|
||||
pkgkey,
|
||||
...(integration ? { integration } : {}),
|
||||
...(agentPolicyIdFromContext
|
||||
? { agentPolicyId: agentPolicyIdFromContext }
|
||||
: {}),
|
||||
})}
|
||||
missingSecurityConfiguration={missingSecurityConfiguration}
|
||||
packageName={integrationInfo?.title || packageInfo.title}
|
||||
onClick={handleAddIntegrationPolicyClick}
|
||||
/>
|
||||
<WithGuidedOnboardingTour
|
||||
packageKey={pkgkey}
|
||||
tourType={'addIntegrationButton'}
|
||||
isGuidedOnboardingActive={isGuidedOnboardingActive}
|
||||
>
|
||||
<AddIntegrationButton
|
||||
userCanInstallPackages={userCanInstallPackages}
|
||||
href={getHref('add_integration_to_policy', {
|
||||
pkgkey,
|
||||
...(integration ? { integration } : {}),
|
||||
...(agentPolicyIdFromContext
|
||||
? { agentPolicyId: agentPolicyIdFromContext }
|
||||
: {}),
|
||||
})}
|
||||
missingSecurityConfiguration={missingSecurityConfiguration}
|
||||
packageName={integrationInfo?.title || packageInfo.title}
|
||||
onClick={handleAddIntegrationPolicyClick}
|
||||
/>
|
||||
</WithGuidedOnboardingTour>
|
||||
),
|
||||
},
|
||||
].map((item, index) => (
|
||||
|
@ -385,6 +401,7 @@ export function Detail() {
|
|||
packageInfo,
|
||||
updateAvailable,
|
||||
isInstalled,
|
||||
isGuidedOnboardingActive,
|
||||
userCanInstallPackages,
|
||||
getHref,
|
||||
pkgkey,
|
||||
|
|
|
@ -22,6 +22,7 @@ describe('getInstallPkgRouteOptions', () => {
|
|||
integration: 'myintegration',
|
||||
pkgkey: 'myintegration-1.0.0',
|
||||
isFirstTimeAgentUser: false,
|
||||
isGuidedOnboardingActive: false,
|
||||
isCloud: false,
|
||||
isExperimentalAddIntegrationPageEnabled: false,
|
||||
};
|
||||
|
@ -51,6 +52,7 @@ describe('getInstallPkgRouteOptions', () => {
|
|||
pkgkey: 'myintegration-1.0.0',
|
||||
agentPolicyId: '12345',
|
||||
isFirstTimeAgentUser: false,
|
||||
isGuidedOnboardingActive: false,
|
||||
isCloud: false,
|
||||
isExperimentalAddIntegrationPageEnabled: false,
|
||||
};
|
||||
|
@ -78,6 +80,7 @@ describe('getInstallPkgRouteOptions', () => {
|
|||
integration: 'myintegration',
|
||||
pkgkey: 'myintegration-1.0.0',
|
||||
isFirstTimeAgentUser: true,
|
||||
isGuidedOnboardingActive: false,
|
||||
isCloud: true,
|
||||
isExperimentalAddIntegrationPageEnabled: true,
|
||||
};
|
||||
|
@ -105,6 +108,7 @@ describe('getInstallPkgRouteOptions', () => {
|
|||
integration: 'myintegration',
|
||||
pkgkey: 'apm-1.0.0',
|
||||
isFirstTimeAgentUser: true,
|
||||
isGuidedOnboardingActive: false,
|
||||
isCloud: true,
|
||||
isExperimentalAddIntegrationPageEnabled: true,
|
||||
};
|
||||
|
@ -137,6 +141,7 @@ describe('getInstallPkgRouteOptions', () => {
|
|||
integration: 'myintegration',
|
||||
pkgkey: 'endpoint-1.0.0',
|
||||
isFirstTimeAgentUser: true,
|
||||
isGuidedOnboardingActive: false,
|
||||
isCloud: true,
|
||||
isExperimentalAddIntegrationPageEnabled: true,
|
||||
};
|
||||
|
|
|
@ -29,6 +29,7 @@ interface GetInstallPkgRouteOptionsParams {
|
|||
isCloud: boolean;
|
||||
isExperimentalAddIntegrationPageEnabled: boolean;
|
||||
isFirstTimeAgentUser: boolean;
|
||||
isGuidedOnboardingActive: boolean;
|
||||
}
|
||||
|
||||
const isPackageExemptFromStepsLayout = (pkgkey: string) =>
|
||||
|
@ -45,13 +46,14 @@ export const getInstallPkgRouteOptions = ({
|
|||
isFirstTimeAgentUser,
|
||||
isCloud,
|
||||
isExperimentalAddIntegrationPageEnabled,
|
||||
isGuidedOnboardingActive,
|
||||
}: GetInstallPkgRouteOptionsParams): [string, { path: string; state: unknown }] => {
|
||||
const integrationOpts: { integration?: string } = integration ? { integration } : {};
|
||||
const packageExemptFromStepsLayout = isPackageExemptFromStepsLayout(pkgkey);
|
||||
const useMultiPageLayout =
|
||||
isExperimentalAddIntegrationPageEnabled &&
|
||||
isCloud &&
|
||||
isFirstTimeAgentUser &&
|
||||
(isFirstTimeAgentUser || isGuidedOnboardingActive) &&
|
||||
!packageExemptFromStepsLayout;
|
||||
const path = pagePathGetters.add_integration_to_policy({
|
||||
pkgkey,
|
||||
|
|
|
@ -28,3 +28,4 @@ export * from './use_agent_policy_refresh';
|
|||
export * from './use_package_installations';
|
||||
export * from './use_agent_enrollment_flyout_data';
|
||||
export * from './use_flyout_context';
|
||||
export * from './use_is_guided_onboarding_active';
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { useStartServices } from '.';
|
||||
|
||||
export const useIsGuidedOnboardingActive = (packageName?: string): boolean => {
|
||||
const [result, setResult] = useState<boolean>(false);
|
||||
const { guidedOnboarding } = useStartServices();
|
||||
const isGuidedOnboardingActiveForIntegration = useObservable(
|
||||
// if guided onboarding is not available, return false
|
||||
guidedOnboarding.guidedOnboardingApi
|
||||
? guidedOnboarding.guidedOnboardingApi.isGuidedOnboardingActiveForIntegration$(packageName)
|
||||
: of(false)
|
||||
);
|
||||
useEffect(() => {
|
||||
setResult(!!isGuidedOnboardingActiveForIntegration);
|
||||
}, [isGuidedOnboardingActiveForIntegration]);
|
||||
|
||||
return result;
|
||||
};
|
|
@ -13,6 +13,8 @@ import { coreMock } from '@kbn/core/public/mocks';
|
|||
import type { IStorage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
|
||||
import { guidedOnboardingMock } from '@kbn/guided-onboarding-plugin/public/mocks';
|
||||
|
||||
import { setHttpClient } from '../hooks/use_request';
|
||||
|
||||
import type { FleetAuthz } from '../../common';
|
||||
|
@ -90,6 +92,7 @@ export const createStartServices = (basePath: string = '/mock'): MockedFleetStar
|
|||
},
|
||||
storage: new Storage(createMockStore()) as jest.Mocked<Storage>,
|
||||
authz: fleetAuthzMock,
|
||||
guidedOnboarding: guidedOnboardingMock.createStart(),
|
||||
};
|
||||
|
||||
configureStartServices(startServices);
|
||||
|
|
|
@ -44,6 +44,7 @@ import type { CloudSetup } from '@kbn/cloud-plugin/public';
|
|||
import type { GlobalSearchPluginSetup } from '@kbn/global-search-plugin/public';
|
||||
|
||||
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
|
||||
import type { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/public';
|
||||
|
||||
import { PLUGIN_ID, INTEGRATIONS_PLUGIN_ID, setupRouteService, appRoutesService } from '../common';
|
||||
import { calculateAuthz, calculatePackagePrivilegesFromCapabilities } from '../common/authz';
|
||||
|
@ -101,6 +102,7 @@ export interface FleetStartDeps {
|
|||
share: SharePluginStart;
|
||||
cloud?: CloudStart;
|
||||
usageCollection?: UsageCollectionStart;
|
||||
guidedOnboarding: GuidedOnboardingPluginStart;
|
||||
}
|
||||
|
||||
export interface FleetStartServices extends CoreStart, Exclude<FleetStartDeps, 'cloud'> {
|
||||
|
@ -110,6 +112,7 @@ export interface FleetStartServices extends CoreStart, Exclude<FleetStartDeps, '
|
|||
discover?: DiscoverStart;
|
||||
spaces?: SpacesPluginStart;
|
||||
authz: FleetAuthz;
|
||||
guidedOnboarding: GuidedOnboardingPluginStart;
|
||||
}
|
||||
|
||||
export class FleetPlugin implements Plugin<FleetSetup, FleetStart, FleetSetupDeps, FleetStartDeps> {
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
{ "path": "../licensing/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/data/tsconfig.json" },
|
||||
{ "path": "../encrypted_saved_objects/tsconfig.json" },
|
||||
{"path": "../../../src/plugins/guided_onboarding/tsconfig.json"},
|
||||
|
||||
// optionalPlugins from ./kibana.json
|
||||
{ "path": "../security/tsconfig.json" },
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue