Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Guided onboarding] Implement quit guide functionality #142542

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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',
Expand All @@ -42,7 +43,9 @@ const mockActiveSearchGuideState: GuideState = {
};

const getGuidePanel = () => () => {
return <GuidePanel application={applicationMock} api={apiService} />;
return (
<GuidePanel application={applicationMock} api={apiService} notifications={notificationsMock} />
);
};

describe('Guided setup', () => {
Expand All @@ -55,6 +58,7 @@ describe('Guided setup', () => {
httpClient.get.mockResolvedValue({
state: [],
});
httpClient.delete.mockResolvedValue({});
apiService.setup(httpClient);

await act(async () => {
Expand Down Expand Up @@ -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);
});
});
});
});
36 changes: 28 additions & 8 deletions src/plugins/guided_onboarding/public/components/guide_panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,21 @@ 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';

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 {
api: ApiService;
application: ApplicationStart;
notifications: NotificationsSetup;
}

const getConfig = (state?: GuideState): GuideConfig | undefined => {
Expand Down Expand Up @@ -81,9 +84,10 @@ 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);
const [guideState, setGuideState] = useState<GuideState | undefined>(undefined);

const styles = getGuidePanelStyles(euiTheme);
Expand All @@ -108,11 +112,20 @@ 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) {
setGuideState(newGuideState);
}
setGuideState(newGuideState);
});
return () => subscription.unsubscribe();
}, [api]);
Expand Down Expand Up @@ -236,7 +249,7 @@ export const GuidePanel = ({ api, application }: GuidePanelProps) => {

<EuiHorizontalRule />

{guideConfig?.steps.map((step, index, steps) => {
{guideConfig?.steps.map((step, index) => {
const accordionId = htmlIdGenerator(`accordion${index}`)();
const stepState = guideState?.steps[index];

Expand Down Expand Up @@ -271,10 +284,9 @@ export const GuidePanel = ({ api, application }: GuidePanelProps) => {
<EuiFlyoutFooter css={styles.flyoutOverrides.flyoutFooter}>
<EuiFlexGroup direction="column" alignItems="center" gutterSize="xs">
<EuiFlexItem>
{/* TODO: Implement exit guide modal - https://github.com/elastic/kibana/issues/139804 */}
<EuiButtonEmpty onClick={() => {}}>
<EuiButtonEmpty onClick={openQuitGuideModal} data-test-subj="quitGuideButton">
{i18n.translate('guidedOnboarding.dropdownPanel.footer.exitGuideButtonLabel', {
defaultMessage: 'Exit setup guide',
defaultMessage: 'Quit setup guide',
})}
</EuiButtonEmpty>
</EuiFlexItem>
Expand Down Expand Up @@ -325,6 +337,14 @@ export const GuidePanel = ({ api, application }: GuidePanelProps) => {
</EuiFlyoutFooter>
</EuiFlyout>
)}

{isQuitGuideModalOpen && (
<QuitGuideModal
closeModal={closeQuitGuideModal}
currentGuide={guideState!.guideId}
notifications={notifications}
/>
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* 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, { useState } from 'react';

import {
EuiModal,
EuiModalBody,
EuiSpacer,
EuiTitle,
EuiText,
EuiModalFooter,
EuiButtonEmpty,
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,
currentGuide,
notifications,
}: QuitGuideModalProps) => {
const [isDeleting, setIsDeleting] = useState<boolean>(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.',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want them to try again right away or should they wait a moment?

Copy link
Contributor Author

@alisonelizabeth alisonelizabeth Oct 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... good question. I suppose it depends on the actual error. Do we have general guidelines around error messages when an API request fails?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message guidance that we've provided the platform (cloud) team in the past is that we want to A. tell them what happened, B. tell them what they can do about it, and C. only use please when we are asking for their patience. This message meets those guidelines, but it's just a matter of timing.

}),
});
}
closeModal();
};

return (
<EuiModal
maxWidth={448}
aria-label="quitGuideModal"
onClose={closeModal}
data-test-subj="quitGuideModal"
>
<EuiModalBody>
<EuiSpacer size="m" />
<EuiTitle size="m">
<h2>
{i18n.translate('guidedOnboarding.quitGuideModal.modalTitle', {
defaultMessage: 'Quit this setup guide and discard progress?',
})}
</h2>
</EuiTitle>
<EuiSpacer size="m" />
<EuiText>
<p>
{i18n.translate('guidedOnboarding.quitGuideModal.modalDescription', {
defaultMessage: 'You can restart anytime, just click Setup guide on the homepage.',
})}
</p>
</EuiText>
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty onClick={closeModal} data-test-subj="cancelQuitGuideButton">
{i18n.translate('guidedOnboarding.quitGuideModal.cancelButtonLabel', {
defaultMessage: 'Cancel',
})}
</EuiButtonEmpty>
<EuiButton
color="warning"
isLoading={isDeleting}
onClick={deleteGuide}
fill
data-test-subj="confirmQuitGuideButton"
>
{i18n.translate('guidedOnboarding.quitGuideModal.quitButtonLabel', {
defaultMessage: 'Quit guide',
})}
</EuiButton>
</EuiModalFooter>
</EuiModal>
);
};
8 changes: 6 additions & 2 deletions src/plugins/guided_onboarding/public/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
CoreTheme,
ApplicationStart,
PluginInitializerContext,
NotificationsSetup,
} from '@kbn/core/public';

import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
Expand All @@ -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);
Expand All @@ -55,6 +56,7 @@ export class GuidedOnboardingPlugin
theme$: theme.theme$,
api: apiService,
application,
notifications,
}),
});

Expand All @@ -71,16 +73,18 @@ export class GuidedOnboardingPlugin
theme$,
api,
application,
notifications,
}: {
targetDomElement: HTMLElement;
theme$: Rx.Observable<CoreTheme>;
api: ApiService;
application: ApplicationStart;
notifications: NotificationsSetup;
}) {
ReactDOM.render(
<KibanaThemeProvider theme$={theme$}>
<I18nProvider>
<GuidePanel api={api} application={application} />
<GuidePanel api={api} application={application} notifications={notifications} />
</I18nProvider>
</KibanaThemeProvider>,
targetDomElement
Expand Down
8 changes: 8 additions & 0 deletions src/plugins/guided_onboarding/public/services/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Loading