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 @@ -228,5 +228,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('confirmModalConfirmButton').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('confirmModalCancelButton').simulate('click');
});

component.update();

expect(exists('quitGuideModal')).toBe(false);
expect(exists('guideButton')).toBe(true);
});
});
});
});
29 changes: 22 additions & 7 deletions src/plugins/guided_onboarding/public/components/guide_panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ import {
import { ApplicationStart } from '@kbn/core-application-browser';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';

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 {
Expand Down Expand Up @@ -84,6 +86,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<GuideState | undefined>(undefined);

const styles = getGuidePanelStyles(euiTheme);
Expand All @@ -108,11 +111,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 +248,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 +283,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 +336,10 @@ export const GuidePanel = ({ api, application }: GuidePanelProps) => {
</EuiFlyoutFooter>
</EuiFlyout>
)}

{isQuitGuideModalOpen && (
<QuitGuideModal closeModal={closeQuitGuideModal} currentGuide={guideState!} />
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* 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 { EuiText, EuiConfirmModal } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { GuideState } from '../../common/types';
import { apiService } from '../services/api';

interface QuitGuideModalProps {
closeModal: () => void;
currentGuide: GuideState;
}

export const QuitGuideModal = ({ closeModal, currentGuide }: QuitGuideModalProps) => {
const [isDeleting, setIsDeleting] = useState<boolean>(false);

const deleteGuide = async () => {
setIsDeleting(true);
await apiService.deactivateGuide(currentGuide);
closeModal();
};

return (
<EuiConfirmModal
maxWidth={448}
title={i18n.translate('guidedOnboarding.quitGuideModal.modalTitle', {
defaultMessage: 'Quit this guide?',
})}
onCancel={closeModal}
onConfirm={deleteGuide}
cancelButtonText={i18n.translate('guidedOnboarding.quitGuideModal.cancelButtonLabel', {
defaultMessage: 'Cancel',
})}
confirmButtonText={i18n.translate('guidedOnboarding.quitGuideModal.quitButtonLabel', {
defaultMessage: 'Quit guide',
})}
aria-label="quitGuideModal"
buttonColor="warning"
isLoading={isDeleting}
data-test-subj="quitGuideModal"
>
<EuiText>
<p>
{i18n.translate('guidedOnboarding.quitGuideModal.modalDescription', {
defaultMessage:
'You can restart anytime by opening the Setup guide from the Help menu.',
})}
</p>
</EuiText>
</EuiConfirmModal>
);
};
14 changes: 14 additions & 0 deletions src/plugins/guided_onboarding/public/services/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,20 @@ describe('GuidedOnboarding ApiService', () => {
});
});

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,
}),
});
});
});

describe('updateGuideState', () => {
it('sends a request to the put API', async () => {
const updatedState: GuideState = {
Expand Down
19 changes: 18 additions & 1 deletion src/plugins/guided_onboarding/public/services/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,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) {
Expand Down Expand Up @@ -152,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
Expand Down
5 changes: 3 additions & 2 deletions src/plugins/guided_onboarding/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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()),
Expand Down Expand Up @@ -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(),
Expand Down