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

[Stateful sidenav] Welcome tour #194926

Merged
merged 28 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
6d42a3b
Register global setting
sebelga Oct 4, 2024
9871ea3
Add tour to the SpacesNavControl
sebelga Oct 4, 2024
ff44a35
Add functional tests
sebelga Oct 4, 2024
bca12fb
Update jest tests
sebelga Oct 4, 2024
54a8364
Dont show the tour after editing the default space and set a solution
sebelga Oct 4, 2024
a828c85
Don't show tour after clicking on "Manage spaces" btn
sebelga Oct 7, 2024
cf246a8
Add more functional tests
sebelga Oct 7, 2024
6e8e7c8
Refactor
sebelga Oct 7, 2024
77d3eb6
Move tour logic to own folder and component
sebelga Oct 7, 2024
c9fca06
Fix functional test
sebelga Oct 7, 2024
12f4a9c
Merge branch 'main' into stateful-sidenav/welcome-tour
sebelga Oct 8, 2024
5d92a01
Merge remote-tracking branch 'upstream/main' into stateful-sidenav/we…
sebelga Oct 8, 2024
14d5974
Move the learn more btn inline
sebelga Oct 8, 2024
862eb61
Merge branch 'main' into stateful-sidenav/welcome-tour
sebelga Oct 9, 2024
eae5945
Update x-pack/plugins/spaces/public/nav_control/solution_view_tour/li…
sebelga Oct 9, 2024
b66d608
Make it more explicit that the tour is not shown when solution is cla…
sebelga Oct 9, 2024
d8ac760
Merge remote-tracking branch 'upstream/main' into stateful-sidenav/we…
sebelga Oct 9, 2024
954137d
Refactor using OnBoardingDefaultSolution
sebelga Oct 9, 2024
d7586fe
Fix jest test
sebelga Oct 9, 2024
a30e165
Merge remote-tracking branch 'upstream/main' into stateful-sidenav/we…
sebelga Oct 11, 2024
edc1252
Don't load spaces if the tour has been dismissed
sebelga Oct 11, 2024
0889760
Address CR feedback
sebelga Oct 11, 2024
7d2d8a7
Address CR changes
sebelga Oct 11, 2024
ed1d86e
Update snapshot
sebelga Oct 11, 2024
fc00407
Merge branch 'main' into stateful-sidenav/welcome-tour
sebelga Oct 13, 2024
91a8d71
Address CR changes
sebelga Oct 15, 2024
e0a6a18
Merge branch 'stateful-sidenav/welcome-tour' of github.com:sebelga/ki…
sebelga Oct 15, 2024
bdd0ea4
Merge remote-tracking branch 'upstream/main' into stateful-sidenav/we…
sebelga Oct 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions x-pack/plugins/spaces/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,8 @@ export const API_VERSIONS = {
v1: '2023-10-31',
},
};

/**
* The setting to control whether the Space Solution Tour is shown.
*/
export const SHOW_SPACE_SOLUTION_TOUR_SETTING = 'showSpaceSolutionTour';
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { getSpacesFeatureDescription } from '../../constants';
interface Props {
id: string;
isLoading: boolean;
toggleSpaceSelector: () => void;
onClickManageSpaceBtn: () => void;
capabilities: Capabilities;
navigateToApp: ApplicationStart['navigateToApp'];
}
Expand All @@ -45,7 +45,7 @@ export const SpacesDescription: FC<Props> = (props: Props) => {
<ManageSpacesButton
size="s"
style={{ width: `100%` }}
onClick={props.toggleSpaceSelector}
onClick={props.onClickManageSpaceBtn}
capabilities={props.capabilities}
navigateToApp={props.navigateToApp}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ interface Props {
spaces: Space[];
serverBasePath: string;
toggleSpaceSelector: () => void;
onClickManageSpaceBtn: () => void;
intl: InjectedIntl;
capabilities: Capabilities;
navigateToApp: ApplicationStart['navigateToApp'];
Expand Down Expand Up @@ -218,7 +219,7 @@ class SpacesMenuUI extends Component<Props> {
key="manageSpacesButton"
className="spcMenu__manageButton"
size="s"
onClick={this.props.toggleSpaceSelector}
onClick={this.props.onClickManageSpaceBtn}
capabilities={this.props.capabilities}
navigateToApp={this.props.navigateToApp}
/>
Expand Down
5 changes: 5 additions & 0 deletions x-pack/plugins/spaces/public/nav_control/nav_control.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import ReactDOM from 'react-dom';
import type { CoreStart } from '@kbn/core/public';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';

import { initTour } from './solution_view_tour';
import type { EventTracker } from '../analytics';
import type { ConfigType } from '../config';
import type { SpacesManager } from '../spaces_manager';
Expand All @@ -22,6 +23,8 @@ export function initSpacesNavControl(
config: ConfigType,
eventTracker: EventTracker
) {
const { showTour$, onFinishTour } = initTour(core, spacesManager);

core.chrome.navControls.registerLeft({
order: 1000,
mount(targetDomElement: HTMLElement) {
Expand All @@ -47,6 +50,8 @@ export function initSpacesNavControl(
navigateToUrl={core.application.navigateToUrl}
allowSolutionVisibility={config.allowSolutionVisibility}
eventTracker={eventTracker}
showTour$={showTour$}
onFinishTour={onFinishTour}
/>
</Suspense>
</KibanaRenderContextProvider>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import {
EuiFieldSearch,
EuiHeaderSectionItemButton,
EuiPopover,
EuiSelectable,
EuiSelectableListItem,
} from '@elastic/eui';
Expand All @@ -18,7 +17,7 @@ import * as Rx from 'rxjs';

import { findTestSubject, mountWithIntl } from '@kbn/test-jest-helpers';

import { NavControlPopover } from './nav_control_popover';
import { NavControlPopover, type Props as NavControlPopoverProps } from './nav_control_popover';
import type { Space } from '../../common';
import { EventTracker } from '../analytics';
import { SpaceAvatarInternal } from '../space_avatar/space_avatar_internal';
Expand Down Expand Up @@ -49,7 +48,12 @@ const reportEvent = jest.fn();
const eventTracker = new EventTracker({ reportEvent });

describe('NavControlPopover', () => {
async function setup(spaces: Space[], allowSolutionVisibility = false, activeSpace?: Space) {
async function setup(
spaces: Space[],
allowSolutionVisibility = false,
activeSpace?: Space,
props?: Partial<NavControlPopoverProps>
) {
const spacesManager = spacesManagerMock.create();
spacesManager.getSpaces = jest.fn().mockResolvedValue(spaces);

Expand All @@ -68,6 +72,9 @@ describe('NavControlPopover', () => {
navigateToUrl={jest.fn()}
allowSolutionVisibility={allowSolutionVisibility}
eventTracker={eventTracker}
showTour$={Rx.of(false)}
onFinishTour={jest.fn()}
{...props}
/>
);

Expand All @@ -81,7 +88,7 @@ describe('NavControlPopover', () => {
it('renders without crashing', () => {
const spacesManager = spacesManagerMock.create();

const { baseElement } = render(
const { baseElement, queryByTestId } = render(
<NavControlPopover
spacesManager={spacesManager as unknown as SpacesManager}
serverBasePath={'/server-base-path'}
Expand All @@ -91,9 +98,12 @@ describe('NavControlPopover', () => {
navigateToUrl={jest.fn()}
allowSolutionVisibility={false}
eventTracker={eventTracker}
showTour$={Rx.of(false)}
onFinishTour={jest.fn()}
/>
);
expect(baseElement).toMatchSnapshot();
expect(queryByTestId('spaceSolutionTour')).toBeNull();
});

it('renders a SpaceAvatar with the active space', async () => {
Expand All @@ -117,6 +127,8 @@ describe('NavControlPopover', () => {
navigateToUrl={jest.fn()}
allowSolutionVisibility={false}
eventTracker={eventTracker}
showTour$={Rx.of(false)}
onFinishTour={jest.fn()}
/>
);

Expand Down Expand Up @@ -223,20 +235,29 @@ describe('NavControlPopover', () => {
});

it('can close its popover', async () => {
jest.useFakeTimers();
const wrapper = await setup(mockSpaces);

expect(findTestSubject(wrapper, 'spaceMenuPopoverPanel').exists()).toEqual(false); // closed

// Open the popover
await act(async () => {
wrapper.find(EuiHeaderSectionItemButton).find('button').simulate('click');
});
wrapper.update();
expect(wrapper.find(EuiPopover).props().isOpen).toEqual(true);
expect(findTestSubject(wrapper, 'spaceMenuPopoverPanel').exists()).toEqual(true); // open

// Close the popover
await act(async () => {
wrapper.find(EuiPopover).props().closePopover();
wrapper.find(EuiHeaderSectionItemButton).find('button').simulate('click');
});
act(() => {
jest.runAllTimers();
});
wrapper.update();
expect(findTestSubject(wrapper, 'spaceMenuPopoverPanel').exists()).toEqual(false); // closed

expect(wrapper.find(EuiPopover).props().isOpen).toEqual(false);
jest.useRealTimers();
});

it('should render solution for spaces', async () => {
Expand Down Expand Up @@ -301,4 +322,42 @@ describe('NavControlPopover', () => {
space_id_prev: 'space-1',
});
});

it('should show the solution view tour', async () => {
jest.useFakeTimers(); // the underlying EUI tour component has a timeout that needs to be flushed for the test to pass

const spaces: Space[] = [
{
id: 'space-1',
name: 'Space-1',
disabledFeatures: [],
solution: 'es',
},
];

const activeSpace = spaces[0];
const showTour$ = new Rx.BehaviorSubject(true);
const onFinishTour = jest.fn().mockImplementation(() => {
showTour$.next(false);
});

const wrapper = await setup(spaces, true /** allowSolutionVisibility **/, activeSpace, {
showTour$,
onFinishTour,
});

expect(findTestSubject(wrapper, 'spaceSolutionTour').exists()).toBe(true);

act(() => {
findTestSubject(wrapper, 'closeTourBtn').simulate('click');
});
act(() => {
jest.runAllTimers();
});
wrapper.update();

expect(findTestSubject(wrapper, 'spaceSolutionTour').exists()).toBe(false);

jest.useRealTimers();
});
});
63 changes: 48 additions & 15 deletions x-pack/plugins/spaces/public/nav_control/nav_control_popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ import {
withEuiTheme,
} from '@elastic/eui';
import React, { Component, lazy, Suspense } from 'react';
import type { Subscription } from 'rxjs';
import type { Observable, Subscription } from 'rxjs';

import type { ApplicationStart, Capabilities } from '@kbn/core/public';
import { i18n } from '@kbn/i18n';

import { SpacesDescription } from './components/spaces_description';
import { SpacesMenu } from './components/spaces_menu';
import { SolutionViewTour } from './solution_view_tour';
import type { Space } from '../../common';
import type { EventTracker } from '../analytics';
import { getSpaceAvatarComponent } from '../space_avatar';
Expand All @@ -30,7 +31,7 @@ const LazySpaceAvatar = lazy(() =>
getSpaceAvatarComponent().then((component) => ({ default: component }))
);

interface Props {
export interface Props {
spacesManager: SpacesManager;
anchorPosition: PopoverAnchorPosition;
capabilities: Capabilities;
Expand All @@ -40,19 +41,23 @@ interface Props {
theme: WithEuiThemeProps['theme'];
allowSolutionVisibility: boolean;
eventTracker: EventTracker;
showTour$: Observable<boolean>;
onFinishTour: () => void;
}

interface State {
showSpaceSelector: boolean;
loading: boolean;
activeSpace: Space | null;
spaces: Space[];
showTour: boolean;
}

const popoutContentId = 'headerSpacesMenuContent';

class NavControlPopoverUI extends Component<Props, State> {
private activeSpace$?: Subscription;
private showTour$Sub?: Subscription;

constructor(props: Props) {
super(props);
Expand All @@ -61,6 +66,7 @@ class NavControlPopoverUI extends Component<Props, State> {
loading: false,
activeSpace: null,
spaces: [],
showTour: false,
};
}

Expand All @@ -72,25 +78,37 @@ class NavControlPopoverUI extends Component<Props, State> {
});
},
});

this.showTour$Sub = this.props.showTour$.subscribe((showTour) => {
this.setState({ showTour });
});
}

public componentWillUnmount() {
this.activeSpace$?.unsubscribe();
this.showTour$Sub?.unsubscribe();
}

public render() {
const button = this.getActiveSpaceButton();
const { theme } = this.props;
const { activeSpace } = this.state;

const isTourOpen = Boolean(activeSpace) && this.state.showTour && !this.state.showSpaceSelector;

let element: React.ReactNode;
if (this.state.loading || this.state.spaces.length < 2) {
element = (
<SpacesDescription
id={popoutContentId}
isLoading={this.state.loading}
toggleSpaceSelector={this.toggleSpaceSelector}
capabilities={this.props.capabilities}
navigateToApp={this.props.navigateToApp}
onClickManageSpaceBtn={() => {
// No need to show the tour anymore, the user is taking action
this.props.onFinishTour();
this.toggleSpaceSelector();
}}
/>
);
} else {
Expand All @@ -106,24 +124,38 @@ class NavControlPopoverUI extends Component<Props, State> {
activeSpace={this.state.activeSpace}
allowSolutionVisibility={this.props.allowSolutionVisibility}
eventTracker={this.props.eventTracker}
onClickManageSpaceBtn={() => {
// No need to show the tour anymore, the user is taking action
this.props.onFinishTour();
this.toggleSpaceSelector();
}}
/>
);
}

return (
<EuiPopover
id="spcMenuPopover"
button={button}
isOpen={this.state.showSpaceSelector}
closePopover={this.closeSpaceSelector}
anchorPosition={this.props.anchorPosition}
panelPaddingSize="none"
repositionOnScroll
ownFocus
zIndex={Number(theme.euiTheme.levels.navigation) + 1} // it needs to sit above the collapsible nav menu
<SolutionViewTour
solution={activeSpace?.solution}
isTourOpen={isTourOpen}
onFinishTour={this.props.onFinishTour}
>
{element}
</EuiPopover>
<EuiPopover
id="spcMenuPopover"
button={button}
isOpen={this.state.showSpaceSelector}
closePopover={this.closeSpaceSelector}
anchorPosition={this.props.anchorPosition}
panelPaddingSize="none"
repositionOnScroll
ownFocus
zIndex={Number(theme.euiTheme.levels.navigation) + 1} // it needs to sit above the collapsible nav menu
panelProps={{
'data-test-subj': 'spaceMenuPopoverPanel',
}}
>
{element}
</EuiPopover>
</SolutionViewTour>
);
}

Expand Down Expand Up @@ -195,6 +227,7 @@ class NavControlPopoverUI extends Component<Props, State> {

protected toggleSpaceSelector = () => {
const isOpening = !this.state.showSpaceSelector;

if (isOpening) {
this.loadSpaces();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* 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.
*/

export { initTour } from './lib';

export { SolutionViewTour } from './solution_view_tour';
Loading