Skip to content

Commit

Permalink
[Search Session] Revamp search session indicator UI and tour (#89703) (
Browse files Browse the repository at this point in the history
  • Loading branch information
Dosant authored Feb 4, 2021
1 parent 378764b commit 012d453
Show file tree
Hide file tree
Showing 21 changed files with 616 additions and 183 deletions.
1 change: 0 additions & 1 deletion src/plugins/dashboard/public/application/dashboard_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,6 @@ export function DashboardApp({

subscriptions.add(
merge(
data.search.session.onRefresh$,
data.query.timefilter.timefilter.getAutoRefreshFetch$(),
searchSessionIdQuery$
).subscribe(() => {
Expand Down
4 changes: 1 addition & 3 deletions src/plugins/data/public/search/session/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Side Public License, v 1.
*/

import { BehaviorSubject, Subject } from 'rxjs';
import { BehaviorSubject } from 'rxjs';
import { ISessionsClient } from './sessions_client';
import { ISessionService } from './session_service';
import { SearchSessionState } from './search_session_state';
Expand All @@ -32,8 +32,6 @@ export function getSessionServiceMock(): jest.Mocked<ISessionService> {
state$: new BehaviorSubject<SearchSessionState>(SearchSessionState.None).asObservable(),
trackSearch: jest.fn((searchDescriptor) => () => {}),
destroy: jest.fn(),
onRefresh$: new Subject(),
refresh: jest.fn(),
cancel: jest.fn(),
isStored: jest.fn(),
isRestore: jest.fn(),
Expand Down
17 changes: 1 addition & 16 deletions src/plugins/data/public/search/session/session_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import { PublicContract } from '@kbn/utility-types';
import { distinctUntilChanged, map, startWith } from 'rxjs/operators';
import { Observable, Subject, Subscription } from 'rxjs';
import { Observable, Subscription } from 'rxjs';
import { PluginInitializerContext, StartServicesAccessor } from 'kibana/public';
import { UrlGeneratorId, UrlGeneratorStateMapping } from '../../../../share/public/';
import { ConfigSchema } from '../../../config';
Expand Down Expand Up @@ -193,21 +193,6 @@ export class SessionService {
this.searchSessionIndicatorUiConfig = undefined;
}

private refresh$ = new Subject<void>();
/**
* Observable emits when search result refresh was requested
* For example, the UI could have it's own "refresh" button
* Application would use this observable to handle user interaction on that button
*/
public onRefresh$ = this.refresh$.asObservable();

/**
* Request a search results refresh
*/
public refresh() {
this.refresh$.next();
}

/**
* Request a cancellation of on-going search requests within current session
*/
Expand Down
7 changes: 0 additions & 7 deletions src/plugins/discover/public/application/angular/discover.js
Original file line number Diff line number Diff line change
Expand Up @@ -504,13 +504,6 @@ function discoverController($route, $scope, Promise) {
)
);

subscriptions.add(
data.search.session.onRefresh$.subscribe(() => {
searchSessionManager.removeSearchSessionIdFromURL({ replace: false });
refetch$.next();
})
);

$scope.changeInterval = (interval) => {
if (interval) {
setAppState({ interval });
Expand Down
10 changes: 10 additions & 0 deletions test/functional/services/common/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,16 @@ export async function BrowserProvider({ getService }: FtrProviderContext) {
);
}

/**
* Removes a value in local storage for the focused window/frame.
*
* @param {string} key
* @return {Promise<void>}
*/
public async removeLocalStorageItem(key: string): Promise<void> {
await driver.executeScript('return window.localStorage.removeItem(arguments[0]);', key);
}

/**
* Clears session storage for the focused window/frame.
*
Expand Down
3 changes: 3 additions & 0 deletions x-pack/plugins/data_enhanced/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { registerSearchSessionsMgmt } from './search/sessions_mgmt';
import { toMountPoint } from '../../../../src/plugins/kibana_react/public';
import { createConnectedSearchSessionIndicator } from './search';
import { ConfigSchema } from '../config';
import { Storage } from '../../../../src/plugins/kibana_utils/public';

export interface DataEnhancedSetupDependencies {
bfetch: BfetchPublicSetup;
Expand All @@ -37,6 +38,7 @@ export class DataEnhancedPlugin
implements Plugin<void, void, DataEnhancedSetupDependencies, DataEnhancedStartDependencies> {
private enhancedSearchInterceptor!: EnhancedSearchInterceptor;
private config!: ConfigSchema;
private readonly storage = new Storage(window.localStorage);

constructor(private initializerContext: PluginInitializerContext<ConfigSchema>) {}

Expand Down Expand Up @@ -83,6 +85,7 @@ export class DataEnhancedPlugin
sessionService: plugins.data.search.session,
application: core.application,
timeFilter: plugins.data.query.timefilter.timefilter,
storage: this.storage,
})
)
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
*/

import React from 'react';
import { StubBrowserStorage } from '@kbn/test/jest';
import { render, waitFor, screen, act } from '@testing-library/react';
import { Storage } from '../../../../../../../src/plugins/kibana_utils/public/';
import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks';
import { createConnectedSearchSessionIndicator } from './connected_search_session_indicator';
import { BehaviorSubject } from 'rxjs';
Expand All @@ -17,17 +19,19 @@ import {
TimefilterContract,
} from '../../../../../../../src/plugins/data/public';
import { coreMock } from '../../../../../../../src/core/public/mocks';
import { TOUR_RESTORE_STEP_KEY, TOUR_TAKING_TOO_LONG_STEP_KEY } from './search_session_tour';

const coreStart = coreMock.createStart();
const dataStart = dataPluginMock.createStartContract();
const sessionService = dataStart.search.session as jest.Mocked<ISessionService>;

let storage: Storage;
const refreshInterval$ = new BehaviorSubject<RefreshInterval>({ value: 0, pause: true });
const timeFilter = dataStart.query.timefilter.timefilter as jest.Mocked<TimefilterContract>;
timeFilter.getRefreshIntervalUpdate$.mockImplementation(() => refreshInterval$);
timeFilter.getRefreshInterval.mockImplementation(() => refreshInterval$.getValue());

beforeEach(() => {
storage = new Storage(new StubBrowserStorage());
refreshInterval$.next({ value: 0, pause: true });
sessionService.isSessionStorageReady.mockImplementation(() => true);
sessionService.getSearchSessionIndicatorUiConfig.mockImplementation(() => ({
Expand All @@ -42,21 +46,29 @@ test("shouldn't show indicator in case no active search session", async () => {
sessionService,
application: coreStart.application,
timeFilter,
storage,
});
const { getByTestId, container } = render(<SearchSessionIndicator />);

// make sure `searchSessionIndicator` isn't appearing after some time (lazy-loading)
await expect(
waitFor(() => getByTestId('searchSessionIndicator'), { timeout: 100 })
).rejects.toThrow();
expect(container).toMatchInlineSnapshot(`<div />`);
expect(container).toMatchInlineSnapshot(`
<div>
<div
class="kbnRedirectCrossAppLinks"
/>
</div>
`);
});

test("shouldn't show indicator in case app hasn't opt-in", async () => {
const SearchSessionIndicator = createConnectedSearchSessionIndicator({
sessionService,
application: coreStart.application,
timeFilter,
storage,
});
const { getByTestId, container } = render(<SearchSessionIndicator />);
sessionService.isSessionStorageReady.mockImplementation(() => false);
Expand All @@ -65,7 +77,13 @@ test("shouldn't show indicator in case app hasn't opt-in", async () => {
await expect(
waitFor(() => getByTestId('searchSessionIndicator'), { timeout: 100 })
).rejects.toThrow();
expect(container).toMatchInlineSnapshot(`<div />`);
expect(container).toMatchInlineSnapshot(`
<div>
<div
class="kbnRedirectCrossAppLinks"
/>
</div>
`);
});

test('should show indicator in case there is an active search session', async () => {
Expand All @@ -74,6 +92,7 @@ test('should show indicator in case there is an active search session', async ()
sessionService: { ...sessionService, state$ },
application: coreStart.application,
timeFilter,
storage,
});
const { getByTestId } = render(<SearchSessionIndicator />);

Expand All @@ -98,6 +117,7 @@ test('should be disabled in case uiConfig says so ', async () => {
sessionService: { ...sessionService, state$ },
application: coreStart.application,
timeFilter,
storage,
});

render(<SearchSessionIndicator />);
Expand All @@ -114,6 +134,7 @@ test('should be disabled during auto-refresh', async () => {
sessionService: { ...sessionService, state$ },
application: coreStart.application,
timeFilter,
storage,
});

render(<SearchSessionIndicator />);
Expand All @@ -128,3 +149,107 @@ test('should be disabled during auto-refresh', async () => {

expect(screen.getByTestId('searchSessionIndicator').querySelector('button')).toBeDisabled();
});

describe('tour steps', () => {
describe('loading state', () => {
beforeAll(() => {
jest.useFakeTimers();
});

afterAll(() => {
jest.useRealTimers();
});

test('shows tour step on slow loading with delay', async () => {
const state$ = new BehaviorSubject(SearchSessionState.Loading);
const SearchSessionIndicator = createConnectedSearchSessionIndicator({
sessionService: { ...sessionService, state$ },
application: coreStart.application,
timeFilter,
storage,
});
const rendered = render(<SearchSessionIndicator />);

await waitFor(() => rendered.getByTestId('searchSessionIndicator'));

expect(() => screen.getByTestId('searchSessionIndicatorPopoverContainer')).toThrow();

act(() => {
jest.advanceTimersByTime(10001);
});

expect(screen.getByTestId('searchSessionIndicatorPopoverContainer')).toBeInTheDocument();

act(() => {
jest.advanceTimersByTime(5000);
state$.next(SearchSessionState.Completed);
});

// Open tour should stay on screen after state change
expect(screen.getByTestId('searchSessionIndicatorPopoverContainer')).toBeInTheDocument();

expect(storage.get(TOUR_RESTORE_STEP_KEY)).toBeFalsy();
expect(storage.get(TOUR_TAKING_TOO_LONG_STEP_KEY)).toBeTruthy();
});

test("doesn't show tour step if state changed before delay", async () => {
const state$ = new BehaviorSubject(SearchSessionState.Loading);
const SearchSessionIndicator = createConnectedSearchSessionIndicator({
sessionService: { ...sessionService, state$ },
application: coreStart.application,
timeFilter,
storage,
});
const rendered = render(<SearchSessionIndicator />);

const searchSessionIndicator = await rendered.findByTestId('searchSessionIndicator');
expect(searchSessionIndicator).toBeTruthy();

act(() => {
jest.advanceTimersByTime(3000);
state$.next(SearchSessionState.Completed);
jest.advanceTimersByTime(3000);
});

expect(rendered.queryByTestId('searchSessionIndicatorPopoverContainer')).toBeFalsy();

expect(storage.get(TOUR_RESTORE_STEP_KEY)).toBeFalsy();
expect(storage.get(TOUR_TAKING_TOO_LONG_STEP_KEY)).toBeFalsy();
});
});

test('shows tour step for restored', async () => {
const state$ = new BehaviorSubject(SearchSessionState.Restored);
const SearchSessionIndicator = createConnectedSearchSessionIndicator({
sessionService: { ...sessionService, state$ },
application: coreStart.application,
timeFilter,
storage,
});
const rendered = render(<SearchSessionIndicator />);

await waitFor(() => rendered.getByTestId('searchSessionIndicator'));
expect(screen.getByTestId('searchSessionIndicatorPopoverContainer')).toBeInTheDocument();

expect(storage.get(TOUR_RESTORE_STEP_KEY)).toBeTruthy();
expect(storage.get(TOUR_TAKING_TOO_LONG_STEP_KEY)).toBeTruthy();
});

test("doesn't show tour for irrelevant state", async () => {
const state$ = new BehaviorSubject(SearchSessionState.Completed);
const SearchSessionIndicator = createConnectedSearchSessionIndicator({
sessionService: { ...sessionService, state$ },
application: coreStart.application,
timeFilter,
storage,
});
const rendered = render(<SearchSessionIndicator />);

await waitFor(() => rendered.getByTestId('searchSessionIndicator'));

expect(rendered.queryByTestId('searchSessionIndicatorPopoverContainer')).toBeFalsy();

expect(storage.get(TOUR_RESTORE_STEP_KEY)).toBeFalsy();
expect(storage.get(TOUR_TAKING_TOO_LONG_STEP_KEY)).toBeFalsy();
});
});
Loading

0 comments on commit 012d453

Please sign in to comment.