From ae79b74076324b7d7273b79d0b77817c89a4580c Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Mon, 3 Oct 2022 20:04:14 -0400 Subject: [PATCH 1/9] create quit modal component --- .../public/components/guide_panel.tsx | 22 ++++-- .../public/components/quit_guide_modal.tsx | 67 +++++++++++++++++++ 2 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx diff --git a/src/plugins/guided_onboarding/public/components/guide_panel.tsx b/src/plugins/guided_onboarding/public/components/guide_panel.tsx index bf57d502918d2..c1050d1689882 100644 --- a/src/plugins/guided_onboarding/public/components/guide_panel.tsx +++ b/src/plugins/guided_onboarding/public/components/guide_panel.tsx @@ -36,6 +36,7 @@ import type { GuideConfig, StepConfig } from '../types'; import type { ApiService } from '../services/api'; import { GuideStep } from './guide_panel_step'; +import { QuitGuideModal } from './quit_guide_modal'; import { getGuidePanelStyles } from './guide_panel.styles'; interface GuidePanelProps { @@ -84,6 +85,7 @@ const getProgress = (state?: GuideState): number => { export const GuidePanel = ({ api, application }: GuidePanelProps) => { const { euiTheme } = useEuiTheme(); const [isGuideOpen, setIsGuideOpen] = useState(false); + const [isQuitGuideModalOpen, setIsQuitGuideModalOpen] = useState(false); const [guideState, setGuideState] = useState(undefined); const styles = getGuidePanelStyles(euiTheme); @@ -108,6 +110,17 @@ export const GuidePanel = ({ api, application }: GuidePanelProps) => { await api.completeGuide(guideState!.guideId); }; + const openQuitGuideModal = () => { + // Close the dropdown panel + setIsGuideOpen(false); + // Open the confirmation modal + setIsQuitGuideModalOpen(true); + }; + + const closeQuitGuideModal = () => { + setIsQuitGuideModalOpen(false); + }; + useEffect(() => { const subscription = api.fetchActiveGuideState$().subscribe((newGuideState) => { if (newGuideState) { @@ -236,7 +249,7 @@ export const GuidePanel = ({ api, application }: GuidePanelProps) => { - {guideConfig?.steps.map((step, index, steps) => { + {guideConfig?.steps.map((step, index) => { const accordionId = htmlIdGenerator(`accordion${index}`)(); const stepState = guideState?.steps[index]; @@ -271,10 +284,9 @@ export const GuidePanel = ({ api, application }: GuidePanelProps) => { - {/* TODO: Implement exit guide modal - https://github.com/elastic/kibana/issues/139804 */} - {}}> + {i18n.translate('guidedOnboarding.dropdownPanel.footer.exitGuideButtonLabel', { - defaultMessage: 'Exit setup guide', + defaultMessage: 'Quit setup guide', })} @@ -325,6 +337,8 @@ export const GuidePanel = ({ api, application }: GuidePanelProps) => { )} + + {isQuitGuideModalOpen && } ); }; diff --git a/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx b/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx new file mode 100644 index 0000000000000..f4a3741851992 --- /dev/null +++ b/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React from 'react'; + +import { + EuiModal, + EuiModalBody, + EuiSpacer, + EuiTitle, + EuiText, + EuiModalFooter, + EuiButtonEmpty, + EuiButton, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +interface QuitGuideModalProps { + closeModal: () => void; +} + +export const QuitGuideModal = ({ closeModal }: QuitGuideModalProps) => { + return ( + + + + +

+ {i18n.translate('guidedOnboarding.quitGuideModal.modalTitle', { + defaultMessage: 'Quit this setup guide and discard progress?', + })} +

+
+ + +

+ {i18n.translate('guidedOnboarding.quitGuideModal.modalDescription', { + defaultMessage: 'You can restart anytime, just click Setup guide on the homepage.', + })} +

+
+
+ + + {i18n.translate('guidedOnboarding.quitGuideModal.cancelButtonLabel', { + defaultMessage: 'Cancel', + })} + + { + // TODO implement + }} + fill + > + {i18n.translate('guidedOnboarding.quitGuideModal.quitButtonLabel', { + defaultMessage: 'Quit guide', + })} + + +
+ ); +}; From c5a1fb4b32caf776f383a360e9dd09206f37be08 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Tue, 4 Oct 2022 13:46:45 -0400 Subject: [PATCH 2/9] add delete api --- .../public/components/guide_panel.tsx | 16 +++++--- .../public/components/quit_guide_modal.tsx | 38 +++++++++++++----- .../guided_onboarding/public/plugin.tsx | 8 +++- .../guided_onboarding/public/services/api.ts | 29 +++++++++++++- .../guided_onboarding/server/routes/index.ts | 39 ++++++++++++++++++- 5 files changed, 110 insertions(+), 20 deletions(-) diff --git a/src/plugins/guided_onboarding/public/components/guide_panel.tsx b/src/plugins/guided_onboarding/public/components/guide_panel.tsx index c1050d1689882..5cd04c39d7706 100644 --- a/src/plugins/guided_onboarding/public/components/guide_panel.tsx +++ b/src/plugins/guided_onboarding/public/components/guide_panel.tsx @@ -29,6 +29,7 @@ import { import { ApplicationStart } from '@kbn/core-application-browser'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import type { NotificationsSetup } from '@kbn/core/public'; import { guidesConfig } from '../constants/guides_config'; import type { GuideState, GuideStepIds } from '../../common/types'; import type { GuideConfig, StepConfig } from '../types'; @@ -42,6 +43,7 @@ import { getGuidePanelStyles } from './guide_panel.styles'; interface GuidePanelProps { api: ApiService; application: ApplicationStart; + notifications: NotificationsSetup; } const getConfig = (state?: GuideState): GuideConfig | undefined => { @@ -82,7 +84,7 @@ const getProgress = (state?: GuideState): number => { return 0; }; -export const GuidePanel = ({ api, application }: GuidePanelProps) => { +export const GuidePanel = ({ api, application, notifications }: GuidePanelProps) => { const { euiTheme } = useEuiTheme(); const [isGuideOpen, setIsGuideOpen] = useState(false); const [isQuitGuideModalOpen, setIsQuitGuideModalOpen] = useState(false); @@ -123,9 +125,7 @@ export const GuidePanel = ({ api, application }: GuidePanelProps) => { useEffect(() => { const subscription = api.fetchActiveGuideState$().subscribe((newGuideState) => { - if (newGuideState) { - setGuideState(newGuideState); - } + setGuideState(newGuideState); }); return () => subscription.unsubscribe(); }, [api]); @@ -338,7 +338,13 @@ export const GuidePanel = ({ api, application }: GuidePanelProps) => { )} - {isQuitGuideModalOpen && } + {isQuitGuideModalOpen && ( + + )} ); }; diff --git a/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx b/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx index f4a3741851992..c06f8216107e9 100644 --- a/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx +++ b/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import React from 'react'; +import React, { useState } from 'react'; import { EuiModal, @@ -18,12 +18,38 @@ import { EuiButton, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import type { NotificationsSetup } from '@kbn/core-notifications-browser'; +import { GuideId } from '../../common/types'; +import { apiService } from '../services/api'; interface QuitGuideModalProps { closeModal: () => void; + currentGuide: GuideId; + notifications: NotificationsSetup; } -export const QuitGuideModal = ({ closeModal }: QuitGuideModalProps) => { +export const QuitGuideModal = ({ + closeModal, + currentGuide, + notifications, +}: QuitGuideModalProps) => { + const [isDeleting, setIsDeleting] = useState(false); + + const deleteGuide = async () => { + setIsDeleting(true); + const { error } = await apiService.deleteGuide(currentGuide); + setIsDeleting(false); + + if (error) { + notifications.toasts.addError(error, { + title: i18n.translate('guidedOnboarding.quitGuideModal.errorToastTitle', { + defaultMessage: 'There was an error quitting the guide. Please try again.', + }), + }); + } + closeModal(); + }; + return ( @@ -50,13 +76,7 @@ export const QuitGuideModal = ({ closeModal }: QuitGuideModalProps) => { defaultMessage: 'Cancel', })} - { - // TODO implement - }} - fill - > + {i18n.translate('guidedOnboarding.quitGuideModal.quitButtonLabel', { defaultMessage: 'Quit guide', })} diff --git a/src/plugins/guided_onboarding/public/plugin.tsx b/src/plugins/guided_onboarding/public/plugin.tsx index f74e19a03300f..b6118d318dc47 100755 --- a/src/plugins/guided_onboarding/public/plugin.tsx +++ b/src/plugins/guided_onboarding/public/plugin.tsx @@ -17,6 +17,7 @@ import { CoreTheme, ApplicationStart, PluginInitializerContext, + NotificationsSetup, } from '@kbn/core/public'; import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; @@ -42,7 +43,7 @@ export class GuidedOnboardingPlugin return {}; } - const { chrome, http, theme, application } = core; + const { chrome, http, theme, application, notifications } = core; // Initialize services apiService.setup(http); @@ -55,6 +56,7 @@ export class GuidedOnboardingPlugin theme$: theme.theme$, api: apiService, application, + notifications, }), }); @@ -71,16 +73,18 @@ export class GuidedOnboardingPlugin theme$, api, application, + notifications, }: { targetDomElement: HTMLElement; theme$: Rx.Observable; api: ApiService; application: ApplicationStart; + notifications: NotificationsSetup; }) { ReactDOM.render( - + , targetDomElement diff --git a/src/plugins/guided_onboarding/public/services/api.ts b/src/plugins/guided_onboarding/public/services/api.ts index 1adfaa5d8cc23..67e6a728786d5 100644 --- a/src/plugins/guided_onboarding/public/services/api.ts +++ b/src/plugins/guided_onboarding/public/services/api.ts @@ -15,6 +15,7 @@ import { isLastStep, getGuideConfig } from './helpers'; export class ApiService { private client: HttpSetup | undefined; + private isGuideAbandoned: boolean = false; private onboardingGuideState$!: BehaviorSubject; public isGuidePanelOpen$: BehaviorSubject = new BehaviorSubject(false); @@ -32,7 +33,7 @@ export class ApiService { // TODO add error handling if this.client has not been initialized or request fails return this.onboardingGuideState$.pipe( concatMap((state) => - state === undefined + this.isGuideAbandoned === false && state === undefined ? from( this.client!.get<{ state: GuideState[] }>(`${API_BASE_PATH}/state`, { query: { @@ -70,6 +71,30 @@ export class ApiService { } } + /** + * Async operation to delete a guide + * On the server, the SO is deleted for the selected guide ID + * This is used for the "Quit guide" functionality on the dropdown panel + * @param {GuideId} guideId the id of the guide (one of search, observability, security) + * @return {Promise} a promise with the response or error + */ + public async deleteGuide(guideId: GuideId): Promise<{ response?: GuideState; error?: Error }> { + if (!this.client) { + throw new Error('ApiService has not be initialized.'); + } + + try { + const response = await this.client.delete(`${API_BASE_PATH}/state/${guideId}`); + // Mark the guide as abandoned + this.isGuideAbandoned = true; + // Reset the guide state + this.onboardingGuideState$.next(undefined); + return { response }; + } catch (error) { + return { error }; + } + } + /** * Updates the SO with the updated guide state and refreshes the observables * This is largely used internally and for tests @@ -102,7 +127,7 @@ 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 {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) * @return {Promise} a promise with the updated guide state */ diff --git a/src/plugins/guided_onboarding/server/routes/index.ts b/src/plugins/guided_onboarding/server/routes/index.ts index cce5aad08b1e5..d7339d14e98a5 100755 --- a/src/plugins/guided_onboarding/server/routes/index.ts +++ b/src/plugins/guided_onboarding/server/routes/index.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; import type { IRouter, SavedObjectsClient } from '@kbn/core/server'; +import { API_BASE_PATH } from '../../common/constants'; import type { GuideState } from '../../common/types'; import { guidedSetupSavedObjectsType } from '../saved_objects'; @@ -35,7 +36,7 @@ export function defineRoutes(router: IRouter) { // Fetch all guides state; optionally pass the query param ?active=true to only return the active guide router.get( { - path: '/api/guided_onboarding/state', + path: `${API_BASE_PATH}/state`, validate: { query: schema.object({ active: schema.maybe(schema.boolean()), @@ -69,7 +70,7 @@ export function defineRoutes(router: IRouter) { // will also check any existing active guides and update them to an "inactive" state router.put( { - path: '/api/guided_onboarding/state', + path: `${API_BASE_PATH}/state`, validate: { body: schema.object({ status: schema.string(), @@ -160,4 +161,38 @@ export function defineRoutes(router: IRouter) { } } ); + + // Delete SO for selected guide + router.delete( + { + path: `${API_BASE_PATH}/state/{guideId}`, + validate: { + params: schema.object({ + guideId: schema.string(), + }), + }, + }, + async (context, request, response) => { + const coreContext = await context.core; + const { guideId } = request.params; + const soClient = coreContext.savedObjects.client as SavedObjectsClient; + + const existingGuideSO = await findGuideById(soClient, guideId); + + if (existingGuideSO.total > 0) { + const existingGuide = existingGuideSO.saved_objects[0]; + + await soClient.delete(guidedSetupSavedObjectsType, existingGuide.id); + + return response.ok({ + body: {}, + }); + } else { + // In the case that the SO doesn't exist (unlikely), return successful response + return response.ok({ + body: {}, + }); + } + } + ); } From 0577a89a80694e53b280d94dea73156c0057fc22 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Tue, 4 Oct 2022 15:46:02 -0400 Subject: [PATCH 3/9] add tests --- .../public/components/guide_panel.test.tsx | 57 ++++++++++++++++++- .../public/components/guide_panel.tsx | 2 +- .../public/components/quit_guide_modal.tsx | 19 +++++-- .../public/services/api.test.ts | 8 +++ 4 files changed, 79 insertions(+), 7 deletions(-) diff --git a/src/plugins/guided_onboarding/public/components/guide_panel.test.tsx b/src/plugins/guided_onboarding/public/components/guide_panel.test.tsx index 3506c15fcba35..93ea8005ea259 100644 --- a/src/plugins/guided_onboarding/public/components/guide_panel.test.tsx +++ b/src/plugins/guided_onboarding/public/components/guide_panel.test.tsx @@ -10,7 +10,7 @@ import { act } from 'react-dom/test-utils'; import React from 'react'; import { applicationServiceMock } from '@kbn/core-application-browser-mocks'; -import { httpServiceMock } from '@kbn/core/public/mocks'; +import { httpServiceMock, notificationServiceMock } from '@kbn/core/public/mocks'; import { HttpSetup } from '@kbn/core/public'; import { guidesConfig } from '../constants/guides_config'; @@ -20,6 +20,7 @@ import { GuidePanel } from './guide_panel'; import { registerTestBed, TestBed } from '@kbn/test-jest-helpers'; const applicationMock = applicationServiceMock.createStartContract(); +const notificationsMock = notificationServiceMock.createSetupContract(); const mockActiveSearchGuideState: GuideState = { guideId: 'search', @@ -42,7 +43,9 @@ const mockActiveSearchGuideState: GuideState = { }; const getGuidePanel = () => () => { - return ; + return ( + + ); }; describe('Guided setup', () => { @@ -55,6 +58,7 @@ describe('Guided setup', () => { httpClient.get.mockResolvedValue({ state: [], }); + httpClient.delete.mockResolvedValue({}); apiService.setup(httpClient); await act(async () => { @@ -228,5 +232,54 @@ describe('Guided setup', () => { expect(find('activeStepButtonLabel').text()).toEqual('Continue'); }); }); + + describe('Quit guide modal', () => { + beforeEach(async () => { + const { component, find, exists } = testBed; + + await act(async () => { + // Enable the "search" guide + await apiService.updateGuideState(mockActiveSearchGuideState, true); + }); + + component.update(); + + await act(async () => { + find('quitGuideButton').simulate('click'); + }); + + component.update(); + + expect(exists('quitGuideModal')).toBe(true); + }); + + test('quit a guide', async () => { + const { component, find, exists } = testBed; + + await act(async () => { + find('confirmQuitGuideButton').simulate('click'); + }); + + component.update(); + + expect(exists('quitGuideModal')).toBe(false); + // For now, the guide button is disabled once a user quits a guide + // This behavior will change once https://github.com/elastic/kibana/issues/141129 is implemented + expect(exists('disabledGuideButton')).toBe(true); + }); + + test('cancels out of the quit guide confirmation modal', async () => { + const { component, find, exists } = testBed; + + await act(async () => { + find('cancelQuitGuideButton').simulate('click'); + }); + + component.update(); + + expect(exists('quitGuideModal')).toBe(false); + expect(exists('guideButton')).toBe(true); + }); + }); }); }); diff --git a/src/plugins/guided_onboarding/public/components/guide_panel.tsx b/src/plugins/guided_onboarding/public/components/guide_panel.tsx index 5cd04c39d7706..d59e424cab025 100644 --- a/src/plugins/guided_onboarding/public/components/guide_panel.tsx +++ b/src/plugins/guided_onboarding/public/components/guide_panel.tsx @@ -284,7 +284,7 @@ export const GuidePanel = ({ api, application, notifications }: GuidePanelProps) - + {i18n.translate('guidedOnboarding.dropdownPanel.footer.exitGuideButtonLabel', { defaultMessage: 'Quit setup guide', })} diff --git a/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx b/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx index c06f8216107e9..c8d59565b54c9 100644 --- a/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx +++ b/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx @@ -38,9 +38,9 @@ export const QuitGuideModal = ({ const deleteGuide = async () => { setIsDeleting(true); const { error } = await apiService.deleteGuide(currentGuide); - setIsDeleting(false); if (error) { + setIsDeleting(false); notifications.toasts.addError(error, { title: i18n.translate('guidedOnboarding.quitGuideModal.errorToastTitle', { defaultMessage: 'There was an error quitting the guide. Please try again.', @@ -51,7 +51,12 @@ export const QuitGuideModal = ({ }; return ( - + @@ -71,12 +76,18 @@ export const QuitGuideModal = ({ - + {i18n.translate('guidedOnboarding.quitGuideModal.cancelButtonLabel', { defaultMessage: 'Cancel', })} - + {i18n.translate('guidedOnboarding.quitGuideModal.quitButtonLabel', { defaultMessage: 'Quit guide', })} diff --git a/src/plugins/guided_onboarding/public/services/api.test.ts b/src/plugins/guided_onboarding/public/services/api.test.ts index ffe5596bd7e35..7f4cbd685c364 100644 --- a/src/plugins/guided_onboarding/public/services/api.test.ts +++ b/src/plugins/guided_onboarding/public/services/api.test.ts @@ -84,6 +84,14 @@ describe('GuidedOnboarding ApiService', () => { }); }); + describe('deleteGuide', () => { + it('sends a request to the delete API', async () => { + await apiService.deleteGuide(searchGuide); + expect(httpClient.delete).toHaveBeenCalledTimes(1); + expect(httpClient.delete).toHaveBeenCalledWith(`${API_BASE_PATH}/state/${searchGuide}`); + }); + }); + describe('updateGuideState', () => { it('sends a request to the put API', async () => { const updatedState: GuideState = { From d529025a092ea9112f65dc9fa909f3d0785a8956 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Tue, 4 Oct 2022 21:10:51 -0400 Subject: [PATCH 4/9] Update src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx Co-authored-by: Kelly Murphy --- .../guided_onboarding/public/components/quit_guide_modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx b/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx index c8d59565b54c9..9925ca3356fa6 100644 --- a/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx +++ b/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx @@ -62,7 +62,7 @@ export const QuitGuideModal = ({

{i18n.translate('guidedOnboarding.quitGuideModal.modalTitle', { - defaultMessage: 'Quit this setup guide and discard progress?', + defaultMessage: 'Quit this guide and discard progress?', })}

From ba4036fd36db672258e0163a306fdd1df09e6c03 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Tue, 4 Oct 2022 21:13:47 -0400 Subject: [PATCH 5/9] Update src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx Co-authored-by: Kelly Murphy --- .../guided_onboarding/public/components/quit_guide_modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx b/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx index 9925ca3356fa6..fc806136d6744 100644 --- a/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx +++ b/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx @@ -70,7 +70,7 @@ export const QuitGuideModal = ({

{i18n.translate('guidedOnboarding.quitGuideModal.modalDescription', { - defaultMessage: 'You can restart anytime, just click Setup guide on the homepage.', + defaultMessage: 'You can restart anytime by opening the Setup guide from the Help menu.', })}

From d45c83d23f8da899c4b8594d2bdc2970b85f000e Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 5 Oct 2022 01:21:38 +0000 Subject: [PATCH 6/9] [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' --- .../guided_onboarding/public/components/quit_guide_modal.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx b/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx index fc806136d6744..34e5a2a857f7e 100644 --- a/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx +++ b/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx @@ -70,7 +70,8 @@ export const QuitGuideModal = ({

{i18n.translate('guidedOnboarding.quitGuideModal.modalDescription', { - defaultMessage: 'You can restart anytime by opening the Setup guide from the Help menu.', + defaultMessage: + 'You can restart anytime by opening the Setup guide from the Help menu.', })}

From 57dcbcc16699bc9f67c456e7bf6a32a3856de2fc Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Wed, 5 Oct 2022 09:12:48 -0400 Subject: [PATCH 7/9] cleanup --- .../public/components/quit_guide_modal.tsx | 73 ++++++------------- .../guided_onboarding/public/services/api.ts | 8 +- .../guided_onboarding/server/routes/index.ts | 8 +- 3 files changed, 35 insertions(+), 54 deletions(-) diff --git a/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx b/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx index 34e5a2a857f7e..7cd365c4988c5 100644 --- a/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx +++ b/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx @@ -7,16 +7,7 @@ */ import React, { useState } from 'react'; -import { - EuiModal, - EuiModalBody, - EuiSpacer, - EuiTitle, - EuiText, - EuiModalFooter, - EuiButtonEmpty, - EuiButton, -} from '@elastic/eui'; +import { EuiText, EuiConfirmModal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { NotificationsSetup } from '@kbn/core-notifications-browser'; import { GuideId } from '../../common/types'; @@ -51,49 +42,31 @@ export const QuitGuideModal = ({ }; return ( - - - - -

- {i18n.translate('guidedOnboarding.quitGuideModal.modalTitle', { - defaultMessage: 'Quit this guide and discard progress?', - })} -

-
- - -

- {i18n.translate('guidedOnboarding.quitGuideModal.modalDescription', { - defaultMessage: - 'You can restart anytime by opening the Setup guide from the Help menu.', - })} -

-
-
- - - {i18n.translate('guidedOnboarding.quitGuideModal.cancelButtonLabel', { - defaultMessage: 'Cancel', + +

+ {i18n.translate('guidedOnboarding.quitGuideModal.modalDescription', { + defaultMessage: + 'You can restart anytime by opening the Setup guide from the Help menu.', })} - - - {i18n.translate('guidedOnboarding.quitGuideModal.quitButtonLabel', { - defaultMessage: 'Quit guide', - })} - - - +

+
+ ); }; diff --git a/src/plugins/guided_onboarding/public/services/api.ts b/src/plugins/guided_onboarding/public/services/api.ts index 9a8edafaf3614..09510ffa0b507 100644 --- a/src/plugins/guided_onboarding/public/services/api.ts +++ b/src/plugins/guided_onboarding/public/services/api.ts @@ -84,13 +84,17 @@ export class ApiService implements GuidedOnboardingApi { * @param {GuideId} guideId the id of the guide (one of search, observability, security) * @return {Promise} a promise with the response or error */ - public async deleteGuide(guideId: GuideId): Promise<{ response?: GuideState; error?: Error }> { + public async deleteGuide( + guideId: GuideId + ): Promise<{ response?: { deletedGuide: GuideId }; error?: Error }> { if (!this.client) { throw new Error('ApiService has not be initialized.'); } try { - const response = await this.client.delete(`${API_BASE_PATH}/state/${guideId}`); + const response = await this.client.delete<{ deletedGuide: GuideId }>( + `${API_BASE_PATH}/state/${guideId}` + ); // Mark the guide as abandoned this.isGuideAbandoned = true; // Reset the guide state diff --git a/src/plugins/guided_onboarding/server/routes/index.ts b/src/plugins/guided_onboarding/server/routes/index.ts index d7339d14e98a5..a8b60dc0500e7 100755 --- a/src/plugins/guided_onboarding/server/routes/index.ts +++ b/src/plugins/guided_onboarding/server/routes/index.ts @@ -185,12 +185,16 @@ export function defineRoutes(router: IRouter) { await soClient.delete(guidedSetupSavedObjectsType, existingGuide.id); return response.ok({ - body: {}, + body: { + deletedGuide: guideId, + }, }); } else { // In the case that the SO doesn't exist (unlikely), return successful response return response.ok({ - body: {}, + body: { + deletedGuide: guideId, + }, }); } } From 8e59be984a985dbec68ef3e1377f5e9faf88bfb0 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Wed, 5 Oct 2022 09:20:49 -0400 Subject: [PATCH 8/9] fix tests --- .../guided_onboarding/public/components/guide_panel.test.tsx | 4 ++-- .../guided_onboarding/public/components/quit_guide_modal.tsx | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/plugins/guided_onboarding/public/components/guide_panel.test.tsx b/src/plugins/guided_onboarding/public/components/guide_panel.test.tsx index 93ea8005ea259..1bee057beb4a7 100644 --- a/src/plugins/guided_onboarding/public/components/guide_panel.test.tsx +++ b/src/plugins/guided_onboarding/public/components/guide_panel.test.tsx @@ -257,7 +257,7 @@ describe('Guided setup', () => { const { component, find, exists } = testBed; await act(async () => { - find('confirmQuitGuideButton').simulate('click'); + find('confirmModalConfirmButton').simulate('click'); }); component.update(); @@ -272,7 +272,7 @@ describe('Guided setup', () => { const { component, find, exists } = testBed; await act(async () => { - find('cancelQuitGuideButton').simulate('click'); + find('confirmModalCancelButton').simulate('click'); }); component.update(); diff --git a/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx b/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx index 7cd365c4988c5..4a7beeb048513 100644 --- a/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx +++ b/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx @@ -58,6 +58,7 @@ export const QuitGuideModal = ({ aria-label="quitGuideModal" buttonColor="warning" isLoading={isDeleting} + data-test-subj="quitGuideModal" >

From dc79bcea9ea7c93c5ba09d41b3774ace514cbfaa Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Mon, 10 Oct 2022 09:31:55 -0400 Subject: [PATCH 9/9] address review feedback --- .../public/components/guide_panel.test.tsx | 8 +-- .../public/components/guide_panel.tsx | 11 ++-- .../public/components/quit_guide_modal.tsx | 25 ++-------- .../guided_onboarding/public/plugin.tsx | 8 +-- .../public/services/api.test.ts | 16 ++++-- .../guided_onboarding/public/services/api.ts | 50 +++++++------------ .../guided_onboarding/server/routes/index.ts | 38 -------------- 7 files changed, 42 insertions(+), 114 deletions(-) diff --git a/src/plugins/guided_onboarding/public/components/guide_panel.test.tsx b/src/plugins/guided_onboarding/public/components/guide_panel.test.tsx index 1bee057beb4a7..5bd846aebbdef 100644 --- a/src/plugins/guided_onboarding/public/components/guide_panel.test.tsx +++ b/src/plugins/guided_onboarding/public/components/guide_panel.test.tsx @@ -10,7 +10,7 @@ import { act } from 'react-dom/test-utils'; import React from 'react'; import { applicationServiceMock } from '@kbn/core-application-browser-mocks'; -import { httpServiceMock, notificationServiceMock } from '@kbn/core/public/mocks'; +import { httpServiceMock } from '@kbn/core/public/mocks'; import { HttpSetup } from '@kbn/core/public'; import { guidesConfig } from '../constants/guides_config'; @@ -20,7 +20,6 @@ import { GuidePanel } from './guide_panel'; import { registerTestBed, TestBed } from '@kbn/test-jest-helpers'; const applicationMock = applicationServiceMock.createStartContract(); -const notificationsMock = notificationServiceMock.createSetupContract(); const mockActiveSearchGuideState: GuideState = { guideId: 'search', @@ -43,9 +42,7 @@ const mockActiveSearchGuideState: GuideState = { }; const getGuidePanel = () => () => { - return ( - - ); + return ; }; describe('Guided setup', () => { @@ -58,7 +55,6 @@ describe('Guided setup', () => { httpClient.get.mockResolvedValue({ state: [], }); - httpClient.delete.mockResolvedValue({}); apiService.setup(httpClient); await act(async () => { diff --git a/src/plugins/guided_onboarding/public/components/guide_panel.tsx b/src/plugins/guided_onboarding/public/components/guide_panel.tsx index d59e424cab025..7c122492d84a1 100644 --- a/src/plugins/guided_onboarding/public/components/guide_panel.tsx +++ b/src/plugins/guided_onboarding/public/components/guide_panel.tsx @@ -29,7 +29,7 @@ import { import { ApplicationStart } from '@kbn/core-application-browser'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import type { NotificationsSetup } from '@kbn/core/public'; + import { guidesConfig } from '../constants/guides_config'; import type { GuideState, GuideStepIds } from '../../common/types'; import type { GuideConfig, StepConfig } from '../types'; @@ -43,7 +43,6 @@ import { getGuidePanelStyles } from './guide_panel.styles'; interface GuidePanelProps { api: ApiService; application: ApplicationStart; - notifications: NotificationsSetup; } const getConfig = (state?: GuideState): GuideConfig | undefined => { @@ -84,7 +83,7 @@ const getProgress = (state?: GuideState): number => { return 0; }; -export const GuidePanel = ({ api, application, notifications }: GuidePanelProps) => { +export const GuidePanel = ({ api, application }: GuidePanelProps) => { const { euiTheme } = useEuiTheme(); const [isGuideOpen, setIsGuideOpen] = useState(false); const [isQuitGuideModalOpen, setIsQuitGuideModalOpen] = useState(false); @@ -339,11 +338,7 @@ export const GuidePanel = ({ api, application, notifications }: GuidePanelProps) )} {isQuitGuideModalOpen && ( - + )} ); diff --git a/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx b/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx index 4a7beeb048513..a7a7e34c311b4 100644 --- a/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx +++ b/src/plugins/guided_onboarding/public/components/quit_guide_modal.tsx @@ -9,35 +9,20 @@ import React, { useState } from 'react'; import { EuiText, EuiConfirmModal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import type { NotificationsSetup } from '@kbn/core-notifications-browser'; -import { GuideId } from '../../common/types'; +import { GuideState } from '../../common/types'; import { apiService } from '../services/api'; interface QuitGuideModalProps { closeModal: () => void; - currentGuide: GuideId; - notifications: NotificationsSetup; + currentGuide: GuideState; } -export const QuitGuideModal = ({ - closeModal, - currentGuide, - notifications, -}: QuitGuideModalProps) => { +export const QuitGuideModal = ({ closeModal, currentGuide }: QuitGuideModalProps) => { const [isDeleting, setIsDeleting] = useState(false); const deleteGuide = async () => { setIsDeleting(true); - const { error } = await apiService.deleteGuide(currentGuide); - - if (error) { - setIsDeleting(false); - notifications.toasts.addError(error, { - title: i18n.translate('guidedOnboarding.quitGuideModal.errorToastTitle', { - defaultMessage: 'There was an error quitting the guide. Please try again.', - }), - }); - } + await apiService.deactivateGuide(currentGuide); closeModal(); }; @@ -45,7 +30,7 @@ export const QuitGuideModal = ({ ; api: ApiService; application: ApplicationStart; - notifications: NotificationsSetup; }) { ReactDOM.render( - + , targetDomElement diff --git a/src/plugins/guided_onboarding/public/services/api.test.ts b/src/plugins/guided_onboarding/public/services/api.test.ts index f116a6af153e7..5deb3d50987f2 100644 --- a/src/plugins/guided_onboarding/public/services/api.test.ts +++ b/src/plugins/guided_onboarding/public/services/api.test.ts @@ -72,11 +72,17 @@ describe('GuidedOnboarding ApiService', () => { }); }); - describe('deleteGuide', () => { - it('sends a request to the delete API', async () => { - await apiService.deleteGuide(searchGuide); - expect(httpClient.delete).toHaveBeenCalledTimes(1); - expect(httpClient.delete).toHaveBeenCalledWith(`${API_BASE_PATH}/state/${searchGuide}`); + describe('deactivateGuide', () => { + it('deactivates an existing guide', async () => { + await apiService.deactivateGuide(searchAddDataActiveState); + + expect(httpClient.put).toHaveBeenCalledTimes(1); + expect(httpClient.put).toHaveBeenCalledWith(`${API_BASE_PATH}/state`, { + body: JSON.stringify({ + ...searchAddDataActiveState, + isActive: false, + }), + }); }); }); diff --git a/src/plugins/guided_onboarding/public/services/api.ts b/src/plugins/guided_onboarding/public/services/api.ts index 09510ffa0b507..7c970717be5f1 100644 --- a/src/plugins/guided_onboarding/public/services/api.ts +++ b/src/plugins/guided_onboarding/public/services/api.ts @@ -21,7 +21,6 @@ import type { GuideState, GuideId, GuideStep, GuideStepIds } from '../../common/ export class ApiService implements GuidedOnboardingApi { private client: HttpSetup | undefined; - private isGuideAbandoned: boolean = false; private onboardingGuideState$!: BehaviorSubject; public isGuidePanelOpen$: BehaviorSubject = new BehaviorSubject(false); @@ -39,7 +38,7 @@ export class ApiService implements GuidedOnboardingApi { // TODO add error handling if this.client has not been initialized or request fails return this.onboardingGuideState$.pipe( concatMap((state) => - this.isGuideAbandoned === false && state === undefined + state === undefined ? from( this.client!.get<{ state: GuideState[] }>(`${API_BASE_PATH}/state`, { query: { @@ -77,34 +76,6 @@ export class ApiService implements GuidedOnboardingApi { } } - /** - * Async operation to delete a guide - * On the server, the SO is deleted for the selected guide ID - * This is used for the "Quit guide" functionality on the dropdown panel - * @param {GuideId} guideId the id of the guide (one of search, observability, security) - * @return {Promise} a promise with the response or error - */ - public async deleteGuide( - guideId: GuideId - ): Promise<{ response?: { deletedGuide: GuideId }; error?: Error }> { - if (!this.client) { - throw new Error('ApiService has not be initialized.'); - } - - try { - const response = await this.client.delete<{ deletedGuide: GuideId }>( - `${API_BASE_PATH}/state/${guideId}` - ); - // Mark the guide as abandoned - this.isGuideAbandoned = true; - // Reset the guide state - this.onboardingGuideState$.next(undefined); - return { response }; - } catch (error) { - return { error }; - } - } - /** * Updates the SO with the updated guide state and refreshes the observables * This is largely used internally and for tests @@ -124,7 +95,8 @@ export class ApiService implements GuidedOnboardingApi { const response = await this.client.put<{ state: GuideState }>(`${API_BASE_PATH}/state`, { body: JSON.stringify(newState), }); - this.onboardingGuideState$.next(newState); + // If the guide has been deactivated, we return undefined + this.onboardingGuideState$.next(newState.isActive ? newState : undefined); this.isGuidePanelOpen$.next(panelState); return response; } catch (error) { @@ -181,6 +153,22 @@ export class ApiService implements GuidedOnboardingApi { } } + /** + * Marks a guide as inactive + * This is useful for the dropdown panel, when a user quits a guide + * @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 deactivateGuide(guide: GuideState): Promise<{ state: GuideState } | undefined> { + return await this.updateGuideState( + { + ...guide, + isActive: false, + }, + false + ); + } + /** * Completes a guide * Updates the overall guide status to 'complete', and marks it as inactive diff --git a/src/plugins/guided_onboarding/server/routes/index.ts b/src/plugins/guided_onboarding/server/routes/index.ts index a8b60dc0500e7..adc65d0bf6866 100755 --- a/src/plugins/guided_onboarding/server/routes/index.ts +++ b/src/plugins/guided_onboarding/server/routes/index.ts @@ -161,42 +161,4 @@ export function defineRoutes(router: IRouter) { } } ); - - // Delete SO for selected guide - router.delete( - { - path: `${API_BASE_PATH}/state/{guideId}`, - validate: { - params: schema.object({ - guideId: schema.string(), - }), - }, - }, - async (context, request, response) => { - const coreContext = await context.core; - const { guideId } = request.params; - const soClient = coreContext.savedObjects.client as SavedObjectsClient; - - const existingGuideSO = await findGuideById(soClient, guideId); - - if (existingGuideSO.total > 0) { - const existingGuide = existingGuideSO.saved_objects[0]; - - await soClient.delete(guidedSetupSavedObjectsType, existingGuide.id); - - return response.ok({ - body: { - deletedGuide: guideId, - }, - }); - } else { - // In the case that the SO doesn't exist (unlikely), return successful response - return response.ok({ - body: { - deletedGuide: guideId, - }, - }); - } - } - ); }