Skip to content

Commit 8fbc5cb

Browse files
yuliacechkibanamachinealisonelizabethcindychangy
authored
[Guided onboarding] Manual step completion (#142884)
* [Guided onboarding] Updated the readme file * [Guided onboarding] Updated the security guide config with wording and merged alerts and cases together * [Guided onboarding] Implemented the manual completion for guide steps * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * Revert "[Guided onboarding] Updated the readme file" This reverts commit 5588569. * [Guided onboarding] Reverted the security config changes and added manual step completion * [Guided onboarding] Added jest tests for manual completion * [Guided onboarding] Fixed tests and types errors * Update src/plugins/guided_onboarding/public/services/api.test.ts Co-authored-by: Alison Goryachev <alisonmllr20@gmail.com> * [Guided onboarding] Fixed merge conflict changes and addressed some CR comments * Update src/plugins/guided_onboarding/public/services/helpers.ts Co-authored-by: Alison Goryachev <alisonmllr20@gmail.com> * [Guided onboarding] Addressed more comments * [Guided onboarding] Added completion on navigation * Update src/plugins/guided_onboarding/public/components/guide_button_popover.tsx Co-authored-by: Cindy Chang <cindyisachang@gmail.com> * Update src/plugins/guided_onboarding/public/components/guide_button_popover.tsx Co-authored-by: Cindy Chang <cindyisachang@gmail.com> * [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' * [Guided onboarding] Added a spacer between title and text in the manual step popover Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Alison Goryachev <alisonmllr20@gmail.com> Co-authored-by: Cindy Chang <cindyisachang@gmail.com>
1 parent 5e57ffc commit 8fbc5cb

14 files changed

Lines changed: 461 additions & 198 deletions

File tree

src/plugins/guided_onboarding/common/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@ export type GuideStatus = 'in_progress' | 'ready_to_complete' | 'complete';
2727
* inactive: Step has not started
2828
* active: Step is ready to start (i.e., the guide has been started)
2929
* in_progress: Step has been started and is in progress
30+
* ready_to_complete: Step can be manually completed
3031
* complete: Step has been completed
3132
*/
32-
export type StepStatus = 'inactive' | 'active' | 'in_progress' | 'complete';
33+
export type StepStatus = 'inactive' | 'active' | 'in_progress' | 'ready_to_complete' | 'complete';
3334

3435
export interface GuideStep {
3536
id: GuideStepIds;
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
import React from 'react';
10+
import { EuiButton } from '@elastic/eui';
11+
import { i18n } from '@kbn/i18n';
12+
import { GuideState } from '../../common/types';
13+
import { getStepConfig } from '../services/helpers';
14+
import { GuideButtonPopover } from './guide_button_popover';
15+
16+
interface GuideButtonProps {
17+
guideState: GuideState;
18+
toggleGuidePanel: () => void;
19+
isGuidePanelOpen: boolean;
20+
}
21+
22+
const getStepNumber = (state: GuideState): number | undefined => {
23+
let stepNumber: number | undefined;
24+
25+
state.steps.forEach((step, stepIndex) => {
26+
// If the step is in_progress or ready_to_complete, show that step number
27+
if (step.status === 'in_progress' || step.status === 'ready_to_complete') {
28+
stepNumber = stepIndex + 1;
29+
}
30+
31+
// If the step is active, show the previous step number
32+
if (step.status === 'active') {
33+
stepNumber = stepIndex;
34+
}
35+
});
36+
37+
return stepNumber;
38+
};
39+
40+
export const GuideButton = ({
41+
guideState,
42+
toggleGuidePanel,
43+
isGuidePanelOpen,
44+
}: GuideButtonProps) => {
45+
const stepNumber = getStepNumber(guideState);
46+
const stepReadyToComplete = guideState.steps.find((step) => step.status === 'ready_to_complete');
47+
const button = (
48+
<EuiButton
49+
onClick={toggleGuidePanel}
50+
color="success"
51+
fill
52+
size="s"
53+
data-test-subj="guideButton"
54+
>
55+
{Boolean(stepNumber)
56+
? i18n.translate('guidedOnboarding.guidedSetupStepButtonLabel', {
57+
defaultMessage: 'Setup guide: step {stepNumber}',
58+
values: {
59+
stepNumber,
60+
},
61+
})
62+
: i18n.translate('guidedOnboarding.guidedSetupButtonLabel', {
63+
defaultMessage: 'Setup guide',
64+
})}
65+
</EuiButton>
66+
);
67+
if (stepReadyToComplete) {
68+
const stepConfig = getStepConfig(guideState.guideId, stepReadyToComplete.id);
69+
// check if the stepConfig has manualCompletion info
70+
if (stepConfig && stepConfig.manualCompletion) {
71+
return (
72+
<GuideButtonPopover
73+
button={button}
74+
isGuidePanelOpen={isGuidePanelOpen}
75+
title={stepConfig.manualCompletion.title}
76+
description={stepConfig.manualCompletion.description}
77+
/>
78+
);
79+
}
80+
}
81+
return button;
82+
};
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
import { useEffect, useRef, useState } from 'react';
10+
import { EuiPopover, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
11+
import React from 'react';
12+
13+
interface GuideButtonPopoverProps {
14+
button: EuiPopover['button'];
15+
isGuidePanelOpen: boolean;
16+
title?: string;
17+
description?: string;
18+
}
19+
export const GuideButtonPopover = ({
20+
button,
21+
isGuidePanelOpen,
22+
title,
23+
description,
24+
}: GuideButtonPopoverProps) => {
25+
const isFirstRender = useRef(true);
26+
useEffect(() => {
27+
isFirstRender.current = false;
28+
}, []);
29+
30+
const [isPopoverShown, setIsPopoverShown] = useState(true);
31+
useEffect(() => {
32+
// close the popover after it was rendered once and the panel is opened
33+
if (isGuidePanelOpen && !isFirstRender.current) {
34+
setIsPopoverShown(false);
35+
}
36+
}, [isGuidePanelOpen]);
37+
return (
38+
<EuiPopover
39+
data-test-subj="manualCompletionPopover"
40+
button={button}
41+
isOpen={isPopoverShown}
42+
closePopover={() => {
43+
/* do nothing, the popover is closed once the panel is opened */
44+
}}
45+
>
46+
{title && (
47+
<EuiTitle size="xxs">
48+
<h3>{title}</h3>
49+
</EuiTitle>
50+
)}
51+
<EuiSpacer />
52+
<EuiText style={{ width: 300 }}>{description && <p>{description}</p>}</EuiText>
53+
</EuiPopover>
54+
);
55+
};

src/plugins/guided_onboarding/public/components/guide_panel.test.tsx

Lines changed: 78 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,45 @@ const mockActiveSearchGuideState: GuideState = {
4141
],
4242
};
4343

44+
const mockInProgressSearchGuideState: GuideState = {
45+
...mockActiveSearchGuideState,
46+
steps: [
47+
{
48+
id: mockActiveSearchGuideState.steps[0].id,
49+
status: 'in_progress',
50+
},
51+
mockActiveSearchGuideState.steps[1],
52+
mockActiveSearchGuideState.steps[2],
53+
],
54+
};
55+
56+
const mockReadyToCompleteSearchGuideState: GuideState = {
57+
...mockActiveSearchGuideState,
58+
steps: [
59+
{
60+
id: mockActiveSearchGuideState.steps[0].id,
61+
status: 'complete',
62+
},
63+
{
64+
id: mockActiveSearchGuideState.steps[1].id,
65+
status: 'ready_to_complete',
66+
},
67+
mockActiveSearchGuideState.steps[2],
68+
],
69+
};
70+
71+
const updateComponentWithState = async (
72+
component: TestBed['component'],
73+
guideState: GuideState,
74+
isPanelOpen: boolean
75+
) => {
76+
await act(async () => {
77+
await apiService.updateGuideState(guideState, isPanelOpen);
78+
});
79+
80+
component.update();
81+
};
82+
4483
const getGuidePanel = () => () => {
4584
return <GuidePanel application={applicationMock} api={apiService} />;
4685
};
@@ -80,12 +119,8 @@ describe('Guided setup', () => {
80119
test('should be enabled if there is an active guide', async () => {
81120
const { exists, component, find } = testBed;
82121

83-
await act(async () => {
84-
// Enable the "search" guide
85-
await apiService.updateGuideState(mockActiveSearchGuideState, true);
86-
});
87-
88-
component.update();
122+
// Enable the "search" guide
123+
await updateComponentWithState(component, mockActiveSearchGuideState, true);
89124

90125
expect(exists('disabledGuideButton')).toBe(false);
91126
expect(exists('guideButton')).toBe(true);
@@ -95,38 +130,41 @@ describe('Guided setup', () => {
95130
test('should show the step number in the button label if a step is active', async () => {
96131
const { component, find } = testBed;
97132

98-
const mockInProgressSearchGuideState: GuideState = {
99-
...mockActiveSearchGuideState,
100-
steps: [
101-
{
102-
id: mockActiveSearchGuideState.steps[0].id,
103-
status: 'in_progress',
104-
},
105-
mockActiveSearchGuideState.steps[1],
106-
mockActiveSearchGuideState.steps[2],
107-
],
108-
};
133+
await updateComponentWithState(component, mockInProgressSearchGuideState, true);
109134

110-
await act(async () => {
111-
await apiService.updateGuideState(mockInProgressSearchGuideState, true);
112-
});
135+
expect(find('guideButton').text()).toEqual('Setup guide: step 1');
136+
});
113137

114-
component.update();
138+
test('shows the step number in the button label if a step is ready to complete', async () => {
139+
const { component, find } = testBed;
115140

116-
expect(find('guideButton').text()).toEqual('Setup guide: step 1');
141+
await updateComponentWithState(component, mockReadyToCompleteSearchGuideState, true);
142+
143+
expect(find('guideButton').text()).toEqual('Setup guide: step 2');
144+
});
145+
146+
test('shows the manual completion popover if a step is ready to complete', async () => {
147+
const { component, exists } = testBed;
148+
149+
await updateComponentWithState(component, mockReadyToCompleteSearchGuideState, false);
150+
151+
expect(exists('manualCompletionPopover')).toBe(true);
152+
});
153+
154+
test('shows no manual completion popover if a step is in progress', async () => {
155+
const { component, exists } = testBed;
156+
157+
await updateComponentWithState(component, mockInProgressSearchGuideState, false);
158+
159+
expect(exists('manualCompletionPopoverPanel')).toBe(false);
117160
});
118161
});
119162

120163
describe('Panel component', () => {
121164
test('should be enabled if a guide is activated', async () => {
122165
const { exists, component, find } = testBed;
123166

124-
await act(async () => {
125-
// Enable the "search" guide
126-
await apiService.updateGuideState(mockActiveSearchGuideState, true);
127-
});
128-
129-
component.update();
167+
await updateComponentWithState(component, mockActiveSearchGuideState, true);
130168

131169
expect(exists('guidePanel')).toBe(true);
132170
expect(exists('guideProgress')).toBe(false);
@@ -136,7 +174,7 @@ describe('Guided setup', () => {
136174
test('should show the progress bar if the first step has been completed', async () => {
137175
const { component, exists } = testBed;
138176

139-
const mockInProgressSearchGuideState: GuideState = {
177+
const mockCompleteSearchGuideState: GuideState = {
140178
...mockActiveSearchGuideState,
141179
steps: [
142180
{
@@ -148,11 +186,7 @@ describe('Guided setup', () => {
148186
],
149187
};
150188

151-
await act(async () => {
152-
await apiService.updateGuideState(mockInProgressSearchGuideState, true);
153-
});
154-
155-
component.update();
189+
await updateComponentWithState(component, mockCompleteSearchGuideState, true);
156190

157191
expect(exists('guidePanel')).toBe(true);
158192
expect(exists('guideProgress')).toBe(true);
@@ -181,11 +215,7 @@ describe('Guided setup', () => {
181215
],
182216
};
183217

184-
await act(async () => {
185-
await apiService.updateGuideState(readyToCompleteGuideState, true);
186-
});
187-
188-
component.update();
218+
await updateComponentWithState(component, readyToCompleteGuideState, true);
189219

190220
expect(exists('useElasticButton')).toBe(true);
191221
});
@@ -194,38 +224,25 @@ describe('Guided setup', () => {
194224
test('should show "Start" button label if step has not been started', async () => {
195225
const { component, find } = testBed;
196226

197-
await act(async () => {
198-
// Enable the "search" guide
199-
await apiService.updateGuideState(mockActiveSearchGuideState, true);
200-
});
201-
202-
component.update();
227+
await updateComponentWithState(component, mockActiveSearchGuideState, true);
203228

204229
expect(find('activeStepButtonLabel').text()).toEqual('Start');
205230
});
206231

207232
test('should show "Continue" button label if step is in progress', async () => {
208233
const { component, find } = testBed;
209234

210-
const mockInProgressSearchGuideState: GuideState = {
211-
...mockActiveSearchGuideState,
212-
steps: [
213-
{
214-
id: mockActiveSearchGuideState.steps[0].id,
215-
status: 'in_progress',
216-
},
217-
mockActiveSearchGuideState.steps[1],
218-
mockActiveSearchGuideState.steps[2],
219-
],
220-
};
235+
await updateComponentWithState(component, mockInProgressSearchGuideState, true);
221236

222-
await act(async () => {
223-
await apiService.updateGuideState(mockInProgressSearchGuideState, true);
224-
});
237+
expect(find('activeStepButtonLabel').text()).toEqual('Continue');
238+
});
225239

226-
component.update();
240+
test('shows "Mark done" button label if step is ready to complete', async () => {
241+
const { component, find } = testBed;
227242

228-
expect(find('activeStepButtonLabel').text()).toEqual('Continue');
243+
await updateComponentWithState(component, mockReadyToCompleteSearchGuideState, true);
244+
245+
expect(find('activeStepButtonLabel').text()).toEqual('Mark done');
229246
});
230247
});
231248

0 commit comments

Comments
 (0)