mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
This commit is contained in:
parent
378764bccd
commit
012d453624
21 changed files with 617 additions and 184 deletions
|
@ -192,7 +192,6 @@ export function DashboardApp({
|
|||
|
||||
subscriptions.add(
|
||||
merge(
|
||||
data.search.session.onRefresh$,
|
||||
data.query.timefilter.timefilter.getAutoRefreshFetch$(),
|
||||
searchSessionIdQuery$
|
||||
).subscribe(() => {
|
||||
|
|
|
@ -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';
|
||||
|
@ -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(),
|
||||
|
|
|
@ -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';
|
||||
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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;
|
||||
|
@ -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>) {}
|
||||
|
||||
|
@ -83,6 +85,7 @@ export class DataEnhancedPlugin
|
|||
sessionService: plugins.data.search.session,
|
||||
application: core.application,
|
||||
timeFilter: plugins.data.query.timefilter.timefilter,
|
||||
storage: this.storage,
|
||||
})
|
||||
)
|
||||
),
|
||||
|
|
|
@ -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';
|
||||
|
@ -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(() => ({
|
||||
|
@ -42,6 +46,7 @@ test("shouldn't show indicator in case no active search session", async () => {
|
|||
sessionService,
|
||||
application: coreStart.application,
|
||||
timeFilter,
|
||||
storage,
|
||||
});
|
||||
const { getByTestId, container } = render(<SearchSessionIndicator />);
|
||||
|
||||
|
@ -49,7 +54,13 @@ test("shouldn't show indicator in case no active search session", async () => {
|
|||
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 () => {
|
||||
|
@ -57,6 +68,7 @@ test("shouldn't show indicator in case app hasn't opt-in", async () => {
|
|||
sessionService,
|
||||
application: coreStart.application,
|
||||
timeFilter,
|
||||
storage,
|
||||
});
|
||||
const { getByTestId, container } = render(<SearchSessionIndicator />);
|
||||
sessionService.isSessionStorageReady.mockImplementation(() => false);
|
||||
|
@ -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 () => {
|
||||
|
@ -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 />);
|
||||
|
||||
|
@ -98,6 +117,7 @@ test('should be disabled in case uiConfig says so ', async () => {
|
|||
sessionService: { ...sessionService, state$ },
|
||||
application: coreStart.application,
|
||||
timeFilter,
|
||||
storage,
|
||||
});
|
||||
|
||||
render(<SearchSessionIndicator />);
|
||||
|
@ -114,6 +134,7 @@ test('should be disabled during auto-refresh', async () => {
|
|||
sessionService: { ...sessionService, state$ },
|
||||
application: coreStart.application,
|
||||
timeFilter,
|
||||
storage,
|
||||
});
|
||||
|
||||
render(<SearchSessionIndicator />);
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,33 +5,47 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators';
|
||||
import React, { useRef } from 'react';
|
||||
import { debounce, distinctUntilChanged, map } from 'rxjs/operators';
|
||||
import { timer } from 'rxjs';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SearchSessionIndicator } from '../search_session_indicator';
|
||||
import { ISessionService, TimefilterContract } from '../../../../../../../src/plugins/data/public/';
|
||||
import { SearchSessionIndicator, SearchSessionIndicatorRef } from '../search_session_indicator';
|
||||
import {
|
||||
ISessionService,
|
||||
SearchSessionState,
|
||||
TimefilterContract,
|
||||
} from '../../../../../../../src/plugins/data/public/';
|
||||
import { RedirectAppLinks } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import { ApplicationStart } from '../../../../../../../src/core/public';
|
||||
import { IStorageWrapper } from '../../../../../../../src/plugins/kibana_utils/public';
|
||||
import { useSearchSessionTour } from './search_session_tour';
|
||||
|
||||
export interface SearchSessionIndicatorDeps {
|
||||
sessionService: ISessionService;
|
||||
timeFilter: TimefilterContract;
|
||||
application: ApplicationStart;
|
||||
storage: IStorageWrapper;
|
||||
}
|
||||
|
||||
export const createConnectedSearchSessionIndicator = ({
|
||||
sessionService,
|
||||
application,
|
||||
timeFilter,
|
||||
storage,
|
||||
}: SearchSessionIndicatorDeps): React.FC => {
|
||||
const isAutoRefreshEnabled = () => !timeFilter.getRefreshInterval().pause;
|
||||
const isAutoRefreshEnabled$ = timeFilter
|
||||
.getRefreshIntervalUpdate$()
|
||||
.pipe(map(isAutoRefreshEnabled), distinctUntilChanged());
|
||||
|
||||
const debouncedSessionServiceState$ = sessionService.state$.pipe(
|
||||
debounce((_state) => timer(_state === SearchSessionState.None ? 50 : 300)) // switch to None faster to quickly remove indicator when navigating away
|
||||
);
|
||||
|
||||
return () => {
|
||||
const state = useObservable(sessionService.state$.pipe(debounceTime(500)));
|
||||
const ref = useRef<SearchSessionIndicatorRef>(null);
|
||||
const state = useObservable(debouncedSessionServiceState$, SearchSessionState.None);
|
||||
const autoRefreshEnabled = useObservable(isAutoRefreshEnabled$, isAutoRefreshEnabled());
|
||||
const isDisabledByApp = sessionService.getSearchSessionIndicatorUiConfig().isDisabled();
|
||||
|
||||
|
@ -43,21 +57,28 @@ export const createConnectedSearchSessionIndicator = ({
|
|||
disabledReasonText = i18n.translate(
|
||||
'xpack.data.searchSessionIndicator.disabledDueToAutoRefreshMessage',
|
||||
{
|
||||
defaultMessage: 'Send to background is not available when auto refresh is enabled.',
|
||||
defaultMessage: 'Search sessions are not available when auto refresh is enabled.',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const { markOpenedDone, markRestoredDone } = useSearchSessionTour(
|
||||
storage,
|
||||
ref,
|
||||
state,
|
||||
disabled
|
||||
);
|
||||
|
||||
if (isDisabledByApp.disabled) {
|
||||
disabled = true;
|
||||
disabledReasonText = isDisabledByApp.reasonText;
|
||||
}
|
||||
|
||||
if (!sessionService.isSessionStorageReady()) return null;
|
||||
if (!state) return null;
|
||||
return (
|
||||
<RedirectAppLinks application={application}>
|
||||
<SearchSessionIndicator
|
||||
ref={ref}
|
||||
state={state}
|
||||
onContinueInBackground={() => {
|
||||
sessionService.save();
|
||||
|
@ -65,14 +86,17 @@ export const createConnectedSearchSessionIndicator = ({
|
|||
onSaveResults={() => {
|
||||
sessionService.save();
|
||||
}}
|
||||
onRefresh={() => {
|
||||
sessionService.refresh();
|
||||
}}
|
||||
onCancel={() => {
|
||||
sessionService.cancel();
|
||||
}}
|
||||
disabled={disabled}
|
||||
disabledReasonText={disabledReasonText}
|
||||
onOpened={(openedState) => {
|
||||
markOpenedDone();
|
||||
if (openedState === SearchSessionState.Restored) {
|
||||
markRestoredDone();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</RedirectAppLinks>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { MutableRefObject, useCallback, useEffect } from 'react';
|
||||
import { IStorageWrapper } from '../../../../../../../src/plugins/kibana_utils/public';
|
||||
import { SearchSessionIndicatorRef } from '../search_session_indicator';
|
||||
import { SearchSessionState } from '../../../../../../../src/plugins/data/public';
|
||||
|
||||
const TOUR_TAKING_TOO_LONG_TIMEOUT = 10000;
|
||||
export const TOUR_TAKING_TOO_LONG_STEP_KEY = `data.searchSession.tour.takingTooLong`;
|
||||
export const TOUR_RESTORE_STEP_KEY = `data.searchSession.tour.restore`;
|
||||
|
||||
export function useSearchSessionTour(
|
||||
storage: IStorageWrapper,
|
||||
searchSessionIndicatorRef: MutableRefObject<SearchSessionIndicatorRef | null>,
|
||||
state: SearchSessionState,
|
||||
searchSessionsDisabled: boolean
|
||||
) {
|
||||
const markOpenedDone = useCallback(() => {
|
||||
safeSet(storage, TOUR_TAKING_TOO_LONG_STEP_KEY);
|
||||
}, [storage]);
|
||||
|
||||
const markRestoredDone = useCallback(() => {
|
||||
safeSet(storage, TOUR_RESTORE_STEP_KEY);
|
||||
}, [storage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchSessionsDisabled) return;
|
||||
let timeoutHandle: number;
|
||||
|
||||
if (state === SearchSessionState.Loading) {
|
||||
if (!safeHas(storage, TOUR_TAKING_TOO_LONG_STEP_KEY)) {
|
||||
timeoutHandle = window.setTimeout(() => {
|
||||
safeOpen(searchSessionIndicatorRef);
|
||||
}, TOUR_TAKING_TOO_LONG_TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
if (state === SearchSessionState.Restored) {
|
||||
if (!safeHas(storage, TOUR_RESTORE_STEP_KEY)) {
|
||||
safeOpen(searchSessionIndicatorRef);
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutHandle);
|
||||
};
|
||||
}, [
|
||||
storage,
|
||||
searchSessionIndicatorRef,
|
||||
state,
|
||||
searchSessionsDisabled,
|
||||
markOpenedDone,
|
||||
markRestoredDone,
|
||||
]);
|
||||
|
||||
return {
|
||||
markOpenedDone,
|
||||
markRestoredDone,
|
||||
};
|
||||
}
|
||||
|
||||
function safeHas(storage: IStorageWrapper, key: string): boolean {
|
||||
try {
|
||||
return Boolean(storage.get(key));
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function safeSet(storage: IStorageWrapper, key: string) {
|
||||
try {
|
||||
storage.set(key, true);
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function safeOpen(searchSessionIndicatorRef: MutableRefObject<SearchSessionIndicatorRef | null>) {
|
||||
if (searchSessionIndicatorRef.current) {
|
||||
searchSessionIndicatorRef.current.openPopover();
|
||||
} else {
|
||||
// TODO: needed for initial open when component is not rendered yet
|
||||
// fix after: https://github.com/elastic/eui/issues/4460
|
||||
setTimeout(() => {
|
||||
searchSessionIndicatorRef.current?.openPopover();
|
||||
}, 50);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiIconProps } from '@elastic/eui';
|
||||
|
||||
/**
|
||||
* These are the new icons we've added for search session indicator,
|
||||
* likely in future we will remove these when they land into EUI
|
||||
*/
|
||||
export const CheckInEmptyCircle = ({ title, titleId, ...props }: Omit<EuiIconProps, 'type'>) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
width={16}
|
||||
height={16}
|
||||
aria-labelledby={titleId}
|
||||
{...props}
|
||||
>
|
||||
{title ? <title id={titleId}>{title}</title> : null}
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M15 8c0 3.866-3.134 7-7 7-3.86599 0-7-3.134-7-7 0-3.86599 3.13401-7 7-7 3.866 0 7 3.13401 7 7zm1 0c0 4.4183-3.5817 8-8 8-4.41828 0-8-3.5817-8-8 0-4.41828 3.58172-8 8-8 4.4183 0 8 3.58172 8 8zm-9.14533 2.6459c.098.097.226.146.354.146.128 0 .256-.049.354-.146l4.79173-4.79165c.195-.196.195-.512 0-.708-.196-.195-.512-.195-.708 0L7.20867 9.58486 4.85424 7.2295c-.196-.195-.512-.195-.708 0-.195.196-.195.512 0 .708l2.70843 2.7084z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const PartialClock = ({ title, titleId, ...props }: Omit<EuiIconProps, 'type'>) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
width={16}
|
||||
height={16}
|
||||
aria-labelledby={titleId}
|
||||
{...props}
|
||||
>
|
||||
{title ? <title id={titleId}>{title}</title> : null}
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M7.5 13c3.033 0 5.5-2.467 5.5-5.5S10.533 2 7.5 2c-.27614 0-.5-.22386-.5-.5s.22386-.5.5-.5C11.09 1 14 3.91 14 7.5S11.09 14 7.5 14 1 11.09 1 7.5c0-.27614.22386-.5.5-.5s.5.22386.5.5C2 10.533 4.467 13 7.5 13zM4.6724 1.96808c0 .27614.22386.5.5.5s.5-.22386.5-.5-.22386-.5-.5-.5-.5.22386-.5.5zM2.8627 3.15836c0 .27614.22386.5.5.5s.5-.22386.5-.5c0-.27615-.22386-.5-.5-.5s-.5.22385-.5.5zm-.82355 2.33755c-.27615 0-.5-.22386-.5-.5s.22385-.5.5-.5c.27614 0 .5.22386.5.5s-.22386.5-.5.5zM10.5 7H8V3.5c0-.276-.224-.5-.5-.5s-.5.224-.5.5v4c0 .276.224.5.5.5h3c.276 0 .5-.224.5-.5s-.224-.5-.5-.5z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
|
@ -7,8 +7,11 @@
|
|||
|
||||
import { EuiDelayRender, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import type { SearchSessionIndicatorProps } from './search_session_indicator';
|
||||
export type { SearchSessionIndicatorProps };
|
||||
import type {
|
||||
SearchSessionIndicatorProps,
|
||||
SearchSessionIndicatorRef,
|
||||
} from './search_session_indicator';
|
||||
export type { SearchSessionIndicatorProps, SearchSessionIndicatorRef };
|
||||
|
||||
const Fallback = () => (
|
||||
<EuiDelayRender>
|
||||
|
@ -17,8 +20,11 @@ const Fallback = () => (
|
|||
);
|
||||
|
||||
const LazySearchSessionIndicator = React.lazy(() => import('./search_session_indicator'));
|
||||
export const SearchSessionIndicator = (props: SearchSessionIndicatorProps) => (
|
||||
export const SearchSessionIndicator = React.forwardRef<
|
||||
SearchSessionIndicatorRef,
|
||||
SearchSessionIndicatorProps
|
||||
>((props: SearchSessionIndicatorProps, ref) => (
|
||||
<React.Suspense fallback={<Fallback />}>
|
||||
<LazySearchSessionIndicator {...props} />
|
||||
<LazySearchSessionIndicator {...props} ref={ref} />
|
||||
</React.Suspense>
|
||||
);
|
||||
));
|
||||
|
|
|
@ -2,22 +2,6 @@
|
|||
padding: 0 $euiSizeXS;
|
||||
}
|
||||
|
||||
@include euiBreakpoint('xs', 's') {
|
||||
.searchSessionIndicator__popoverContainer.euiFlexGroup--responsive .euiFlexItem {
|
||||
margin-bottom: $euiSizeXS !important;
|
||||
}
|
||||
}
|
||||
|
||||
.searchSessionIndicator__verticalDivider {
|
||||
@include euiBreakpoint('xs', 's') {
|
||||
margin-left: $euiSizeXS;
|
||||
padding-left: $euiSizeXS;
|
||||
}
|
||||
|
||||
@include euiBreakpoint('m', 'l', 'xl') {
|
||||
border-left: $euiBorderThin;
|
||||
align-self: stretch;
|
||||
margin-left: $euiSizeS;
|
||||
padding-left: $euiSizeS;
|
||||
}
|
||||
.searchSessionIndicator__panel {
|
||||
width: $euiSize * 18;
|
||||
}
|
||||
|
|
|
@ -27,6 +27,9 @@ storiesOf('components/SearchSessionIndicator', module).add('default', () => (
|
|||
<div>
|
||||
<SearchSessionIndicator state={SearchSessionState.Restored} />
|
||||
</div>
|
||||
<div>
|
||||
<SearchSessionIndicator state={SearchSessionState.Canceled} />
|
||||
</div>
|
||||
<div>
|
||||
<SearchSessionIndicator
|
||||
state={SearchSessionState.Completed}
|
||||
|
|
|
@ -24,8 +24,8 @@ test('Loading state', async () => {
|
|||
</Container>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByLabelText('Loading'));
|
||||
await userEvent.click(screen.getByText('Cancel session'));
|
||||
await userEvent.click(screen.getByLabelText('Search session loading'));
|
||||
await userEvent.click(screen.getByText('Stop session'));
|
||||
|
||||
expect(onCancel).toBeCalled();
|
||||
});
|
||||
|
@ -38,7 +38,7 @@ test('Completed state', async () => {
|
|||
</Container>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByLabelText('Loaded'));
|
||||
await userEvent.click(screen.getByLabelText('Search session complete'));
|
||||
await userEvent.click(screen.getByText('Save session'));
|
||||
|
||||
expect(onSave).toBeCalled();
|
||||
|
@ -52,8 +52,8 @@ test('Loading in the background state', async () => {
|
|||
</Container>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByLabelText('Loading results in the background'));
|
||||
await userEvent.click(screen.getByText('Cancel session'));
|
||||
await userEvent.click(screen.getByLabelText(/Saved session in progress/));
|
||||
await userEvent.click(screen.getByText('Stop session'));
|
||||
|
||||
expect(onCancel).toBeCalled();
|
||||
});
|
||||
|
@ -68,38 +68,43 @@ test('BackgroundCompleted state', async () => {
|
|||
</Container>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByLabelText('Results loaded in the background'));
|
||||
expect(screen.getByRole('link', { name: 'View all sessions' }).getAttribute('href')).toBe(
|
||||
await userEvent.click(screen.getByLabelText(/Saved session complete/));
|
||||
expect(screen.getByRole('link', { name: 'Manage sessions' }).getAttribute('href')).toBe(
|
||||
'__link__'
|
||||
);
|
||||
});
|
||||
|
||||
test('Restored state', async () => {
|
||||
const onRefresh = jest.fn();
|
||||
render(
|
||||
<Container>
|
||||
<SearchSessionIndicator state={SearchSessionState.Restored} onRefresh={onRefresh} />
|
||||
<SearchSessionIndicator
|
||||
state={SearchSessionState.Restored}
|
||||
viewSearchSessionsLink={'__link__'}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByLabelText('Results no longer current'));
|
||||
await userEvent.click(screen.getByText('Refresh'));
|
||||
await userEvent.click(screen.getByLabelText(/Saved session restored/));
|
||||
|
||||
expect(onRefresh).toBeCalled();
|
||||
expect(screen.getByRole('link', { name: 'Manage sessions' }).getAttribute('href')).toBe(
|
||||
'__link__'
|
||||
);
|
||||
});
|
||||
|
||||
test('Canceled state', async () => {
|
||||
const onRefresh = jest.fn();
|
||||
render(
|
||||
<Container>
|
||||
<SearchSessionIndicator state={SearchSessionState.Canceled} onRefresh={onRefresh} />
|
||||
<SearchSessionIndicator
|
||||
state={SearchSessionState.Canceled}
|
||||
viewSearchSessionsLink={'__link__'}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByLabelText('Canceled'));
|
||||
await userEvent.click(screen.getByText('Refresh'));
|
||||
|
||||
expect(onRefresh).toBeCalled();
|
||||
await userEvent.click(screen.getByLabelText(/Search session stopped/));
|
||||
expect(screen.getByRole('link', { name: 'Manage sessions' }).getAttribute('href')).toBe(
|
||||
'__link__'
|
||||
);
|
||||
});
|
||||
|
||||
test('Disabled state', async () => {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useCallback, useImperativeHandle } from 'react';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiButtonEmptyProps,
|
||||
|
@ -15,12 +15,13 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiLoadingSpinner,
|
||||
EuiPopover,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { PartialClock, CheckInEmptyCircle } from './custom_icons';
|
||||
import './search_session_indicator.scss';
|
||||
import { SearchSessionState } from '../../../../../../../src/plugins/data/public';
|
||||
|
||||
|
@ -30,9 +31,9 @@ export interface SearchSessionIndicatorProps {
|
|||
onCancel?: () => void;
|
||||
viewSearchSessionsLink?: string;
|
||||
onSaveResults?: () => void;
|
||||
onRefresh?: () => void;
|
||||
disabled?: boolean;
|
||||
disabledReasonText?: string;
|
||||
onOpened?: (openedState: SearchSessionState) => void;
|
||||
}
|
||||
|
||||
type ActionButtonProps = SearchSessionIndicatorProps & { buttonProps: EuiButtonEmptyProps };
|
||||
|
@ -41,11 +42,12 @@ const CancelButton = ({ onCancel = () => {}, buttonProps = {} }: ActionButtonPro
|
|||
<EuiButtonEmpty
|
||||
onClick={onCancel}
|
||||
data-test-subj={'searchSessionIndicatorCancelBtn'}
|
||||
color="danger"
|
||||
{...buttonProps}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.data.searchSessionIndicator.cancelButtonText"
|
||||
defaultMessage="Cancel session"
|
||||
defaultMessage="Stop session"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
@ -61,7 +63,7 @@ const ContinueInBackgroundButton = ({
|
|||
>
|
||||
<FormattedMessage
|
||||
id="xpack.data.searchSessionIndicator.continueInBackgroundButtonText"
|
||||
defaultMessage="Continue in background"
|
||||
defaultMessage="Save session"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
@ -72,25 +74,12 @@ const ViewAllSearchSessionsButton = ({
|
|||
}: ActionButtonProps) => (
|
||||
<EuiButtonEmpty
|
||||
href={viewSearchSessionsLink}
|
||||
data-test-subj={'searchSessionIndicatorviewSearchSessionsLink'}
|
||||
data-test-subj={'searchSessionIndicatorViewSearchSessionsLink'}
|
||||
{...buttonProps}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.data.searchSessionIndicator.viewSearchSessionsLinkText"
|
||||
defaultMessage="View all sessions"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
||||
const RefreshButton = ({ onRefresh = () => {}, buttonProps = {} }: ActionButtonProps) => (
|
||||
<EuiButtonEmpty
|
||||
onClick={onRefresh}
|
||||
data-test-subj={'searchSessionIndicatorRefreshBtn'}
|
||||
{...buttonProps}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.data.searchSessionIndicator.refreshButtonText"
|
||||
defaultMessage="Refresh"
|
||||
defaultMessage="Manage sessions"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
@ -114,7 +103,8 @@ const searchSessionIndicatorViewStateToProps: {
|
|||
tooltipText: string;
|
||||
};
|
||||
popover: {
|
||||
text: string;
|
||||
title: string;
|
||||
description: string;
|
||||
primaryAction?: React.ComponentType<ActionButtonProps>;
|
||||
secondaryAction?: React.ComponentType<ActionButtonProps>;
|
||||
};
|
||||
|
@ -124,19 +114,22 @@ const searchSessionIndicatorViewStateToProps: {
|
|||
[SearchSessionState.Loading]: {
|
||||
button: {
|
||||
color: 'subdued',
|
||||
iconType: 'clock',
|
||||
iconType: PartialClock,
|
||||
'aria-label': i18n.translate(
|
||||
'xpack.data.searchSessionIndicator.loadingResultsIconAriaLabel',
|
||||
{ defaultMessage: 'Loading' }
|
||||
{ defaultMessage: 'Search session loading' }
|
||||
),
|
||||
tooltipText: i18n.translate(
|
||||
'xpack.data.searchSessionIndicator.loadingResultsIconTooltipText',
|
||||
{ defaultMessage: 'Loading' }
|
||||
{ defaultMessage: 'Search session loading' }
|
||||
),
|
||||
},
|
||||
popover: {
|
||||
text: i18n.translate('xpack.data.searchSessionIndicator.loadingResultsText', {
|
||||
defaultMessage: 'Loading',
|
||||
title: i18n.translate('xpack.data.searchSessionIndicator.loadingResultsTitle', {
|
||||
defaultMessage: 'Your search is taking a while...',
|
||||
}),
|
||||
description: i18n.translate('xpack.data.searchSessionIndicator.loadingResultsDescription', {
|
||||
defaultMessage: 'Save your session, continue your work, and return to completed results.',
|
||||
}),
|
||||
primaryAction: CancelButton,
|
||||
secondaryAction: ContinueInBackgroundButton,
|
||||
|
@ -145,21 +138,27 @@ const searchSessionIndicatorViewStateToProps: {
|
|||
[SearchSessionState.Completed]: {
|
||||
button: {
|
||||
color: 'subdued',
|
||||
iconType: 'checkInCircleFilled',
|
||||
iconType: 'clock',
|
||||
'aria-label': i18n.translate('xpack.data.searchSessionIndicator.resultsLoadedIconAriaLabel', {
|
||||
defaultMessage: 'Loaded',
|
||||
defaultMessage: 'Search session complete',
|
||||
}),
|
||||
tooltipText: i18n.translate(
|
||||
'xpack.data.searchSessionIndicator.resultsLoadedIconTooltipText',
|
||||
{
|
||||
defaultMessage: 'Results loaded',
|
||||
defaultMessage: 'Search session complete',
|
||||
}
|
||||
),
|
||||
},
|
||||
popover: {
|
||||
text: i18n.translate('xpack.data.searchSessionIndicator.resultsLoadedText', {
|
||||
defaultMessage: 'Loaded',
|
||||
title: i18n.translate('xpack.data.searchSessionIndicator.resultsLoadedText', {
|
||||
defaultMessage: 'Search session complete',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.data.searchSessionIndicator.resultsLoadedDescriptionText',
|
||||
{
|
||||
defaultMessage: 'Save your session and return to it later.',
|
||||
}
|
||||
),
|
||||
primaryAction: SaveButton,
|
||||
secondaryAction: ViewAllSearchSessionsButton,
|
||||
},
|
||||
|
@ -170,20 +169,26 @@ const searchSessionIndicatorViewStateToProps: {
|
|||
'aria-label': i18n.translate(
|
||||
'xpack.data.searchSessionIndicator.loadingInTheBackgroundIconAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Loading results in the background',
|
||||
defaultMessage: 'Saved session in progress',
|
||||
}
|
||||
),
|
||||
tooltipText: i18n.translate(
|
||||
'xpack.data.searchSessionIndicator.loadingInTheBackgroundIconTooltipText',
|
||||
{
|
||||
defaultMessage: 'Loading results in the background',
|
||||
defaultMessage: 'Saved session in progress',
|
||||
}
|
||||
),
|
||||
},
|
||||
popover: {
|
||||
text: i18n.translate('xpack.data.searchSessionIndicator.loadingInTheBackgroundText', {
|
||||
defaultMessage: 'Loading in the background',
|
||||
title: i18n.translate('xpack.data.searchSessionIndicator.loadingInTheBackgroundTitleText', {
|
||||
defaultMessage: 'Saved session in progress',
|
||||
}),
|
||||
description: i18n.translate(
|
||||
'xpack.data.searchSessionIndicator.loadingInTheBackgroundDescriptionText',
|
||||
{
|
||||
defaultMessage: 'You can return to completed results from Management.',
|
||||
}
|
||||
),
|
||||
primaryAction: CancelButton,
|
||||
secondaryAction: ViewAllSearchSessionsButton,
|
||||
},
|
||||
|
@ -193,74 +198,118 @@ const searchSessionIndicatorViewStateToProps: {
|
|||
color: 'success',
|
||||
iconType: 'checkInCircleFilled',
|
||||
'aria-label': i18n.translate(
|
||||
'xpack.data.searchSessionIndicator.resultLoadedInTheBackgroundIconAraText',
|
||||
'xpack.data.searchSessionIndicator.resultLoadedInTheBackgroundIconAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Results loaded in the background',
|
||||
defaultMessage: 'Saved session complete',
|
||||
}
|
||||
),
|
||||
tooltipText: i18n.translate(
|
||||
'xpack.data.searchSessionIndicator.resultLoadedInTheBackgroundIconTooltipText',
|
||||
{
|
||||
defaultMessage: 'Results loaded in the background',
|
||||
defaultMessage: 'Saved session complete',
|
||||
}
|
||||
),
|
||||
},
|
||||
popover: {
|
||||
text: i18n.translate('xpack.data.searchSessionIndicator.resultLoadedInTheBackgroundText', {
|
||||
defaultMessage: 'Loaded',
|
||||
}),
|
||||
primaryAction: ViewAllSearchSessionsButton,
|
||||
title: i18n.translate(
|
||||
'xpack.data.searchSessionIndicator.resultLoadedInTheBackgroundTitleText',
|
||||
{
|
||||
defaultMessage: 'Search session saved',
|
||||
}
|
||||
),
|
||||
description: i18n.translate(
|
||||
'xpack.data.searchSessionIndicator.resultLoadedInTheBackgroundDescriptionText',
|
||||
{
|
||||
defaultMessage: 'You can return to these results from Management.',
|
||||
}
|
||||
),
|
||||
secondaryAction: ViewAllSearchSessionsButton,
|
||||
},
|
||||
},
|
||||
[SearchSessionState.Restored]: {
|
||||
button: {
|
||||
color: 'warning',
|
||||
iconType: 'refresh',
|
||||
color: 'success',
|
||||
iconType: CheckInEmptyCircle,
|
||||
'aria-label': i18n.translate(
|
||||
'xpack.data.searchSessionIndicator.restoredResultsIconAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Results no longer current',
|
||||
defaultMessage: 'Saved session restored',
|
||||
}
|
||||
),
|
||||
tooltipText: i18n.translate('xpack.data.searchSessionIndicator.restoredResultsTooltipText', {
|
||||
defaultMessage: 'Results no longer current',
|
||||
defaultMessage: 'Search session restored',
|
||||
}),
|
||||
},
|
||||
popover: {
|
||||
text: i18n.translate('xpack.data.searchSessionIndicator.restoredText', {
|
||||
defaultMessage: 'Results no longer current',
|
||||
title: i18n.translate('xpack.data.searchSessionIndicator.restoredTitleText', {
|
||||
defaultMessage: 'Search session restored',
|
||||
}),
|
||||
description: i18n.translate('xpack.data.searchSessionIndicator.restoredDescriptionText', {
|
||||
defaultMessage:
|
||||
'You are viewing cached data from a specific time range. Changing the time range or filters will re-run the session.',
|
||||
}),
|
||||
primaryAction: RefreshButton,
|
||||
secondaryAction: ViewAllSearchSessionsButton,
|
||||
},
|
||||
},
|
||||
[SearchSessionState.Canceled]: {
|
||||
button: {
|
||||
color: 'subdued',
|
||||
iconType: 'refresh',
|
||||
color: 'danger',
|
||||
iconType: 'alert',
|
||||
'aria-label': i18n.translate('xpack.data.searchSessionIndicator.canceledIconAriaLabel', {
|
||||
defaultMessage: 'Canceled',
|
||||
defaultMessage: 'Search session stopped',
|
||||
}),
|
||||
tooltipText: i18n.translate('xpack.data.searchSessionIndicator.canceledTooltipText', {
|
||||
defaultMessage: 'Search was canceled',
|
||||
defaultMessage: 'Search session stopped',
|
||||
}),
|
||||
},
|
||||
popover: {
|
||||
text: i18n.translate('xpack.data.searchSessionIndicator.canceledText', {
|
||||
defaultMessage: 'Search was canceled',
|
||||
title: i18n.translate('xpack.data.searchSessionIndicator.canceledTitleText', {
|
||||
defaultMessage: 'Search session stopped',
|
||||
}),
|
||||
description: i18n.translate('xpack.data.searchSessionIndicator.canceledDescriptionText', {
|
||||
defaultMessage: 'You are viewing incomplete data.',
|
||||
}),
|
||||
primaryAction: RefreshButton,
|
||||
secondaryAction: ViewAllSearchSessionsButton,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const VerticalDivider: React.FC = () => <div className="searchSessionIndicator__verticalDivider" />;
|
||||
export interface SearchSessionIndicatorRef {
|
||||
openPopover: () => void;
|
||||
closePopover: () => void;
|
||||
}
|
||||
|
||||
export const SearchSessionIndicator: React.FC<SearchSessionIndicatorProps> = (props) => {
|
||||
export const SearchSessionIndicator = React.forwardRef<
|
||||
SearchSessionIndicatorRef,
|
||||
SearchSessionIndicatorProps
|
||||
>((props, ref) => {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
|
||||
const onButtonClick = () => setIsPopoverOpen((isOpen) => !isOpen);
|
||||
const closePopover = () => setIsPopoverOpen(false);
|
||||
const closePopover = useCallback(() => setIsPopoverOpen(false), []);
|
||||
const onOpened = props.onOpened;
|
||||
const openPopover = useCallback(() => {
|
||||
setIsPopoverOpen(true);
|
||||
if (onOpened) onOpened(props.state);
|
||||
}, [onOpened, props.state]);
|
||||
const onButtonClick = useCallback(() => {
|
||||
if (isPopoverOpen) {
|
||||
closePopover();
|
||||
} else {
|
||||
openPopover();
|
||||
}
|
||||
}, [isPopoverOpen, openPopover, closePopover]);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
openPopover: () => {
|
||||
openPopover();
|
||||
},
|
||||
closePopover: () => {
|
||||
closePopover();
|
||||
},
|
||||
}),
|
||||
[openPopover, closePopover]
|
||||
);
|
||||
|
||||
if (!searchSessionIndicatorViewStateToProps[props.state]) return null;
|
||||
|
||||
|
@ -271,13 +320,18 @@ export const SearchSessionIndicator: React.FC<SearchSessionIndicatorProps> = (pr
|
|||
ownFocus
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
anchorPosition={'rightCenter'}
|
||||
panelPaddingSize={'s'}
|
||||
anchorPosition={'downLeft'}
|
||||
panelPaddingSize={'m'}
|
||||
className="searchSessionIndicator"
|
||||
data-test-subj={'searchSessionIndicator'}
|
||||
data-state={props.state}
|
||||
panelClassName={'searchSessionIndicator__panel'}
|
||||
repositionOnScroll={true}
|
||||
button={
|
||||
<EuiToolTip content={props.disabled ? props.disabledReasonText : button.tooltipText}>
|
||||
<EuiToolTip
|
||||
content={props.disabled ? props.disabledReasonText : button.tooltipText}
|
||||
delay={props.disabled ? 'regular' : 'long'}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
color={button.color}
|
||||
aria-label={button['aria-label']}
|
||||
|
@ -288,37 +342,37 @@ export const SearchSessionIndicator: React.FC<SearchSessionIndicatorProps> = (pr
|
|||
</EuiToolTip>
|
||||
}
|
||||
>
|
||||
<EuiFlexGroup
|
||||
responsive={true}
|
||||
alignItems={'center'}
|
||||
gutterSize={'s'}
|
||||
className="searchSessionIndicator__popoverContainer"
|
||||
data-test-subj={'searchSessionIndicatorPopoverContainer'}
|
||||
>
|
||||
<EuiFlexItem grow={true}>
|
||||
<EuiText size="s" color={'subdued'}>
|
||||
<p>{popover.text}</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup wrap={true} responsive={false} alignItems={'center'} gutterSize={'s'}>
|
||||
{popover.primaryAction && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<popover.primaryAction {...props} buttonProps={{ size: 'xs', flush: 'both' }} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{popover.primaryAction && popover.secondaryAction && <VerticalDivider />}
|
||||
{popover.secondaryAction && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<popover.secondaryAction {...props} buttonProps={{ size: 'xs', flush: 'both' }} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<div data-test-subj="searchSessionIndicatorPopoverContainer">
|
||||
<EuiText size="s">
|
||||
<p>{popover.title}</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size={'xs'} />
|
||||
<EuiText size="xs" color={'subdued'}>
|
||||
<p>{popover.description}</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size={'s'} />
|
||||
<EuiFlexGroup
|
||||
wrap={true}
|
||||
responsive={false}
|
||||
alignItems={'center'}
|
||||
justifyContent={'flexEnd'}
|
||||
gutterSize={'s'}
|
||||
>
|
||||
{popover.primaryAction && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<popover.primaryAction {...props} buttonProps={{ size: 'xs' }} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{popover.secondaryAction && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<popover.secondaryAction {...props} buttonProps={{ size: 'xs', flush: 'right' }} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
// React.lazy() needs default:
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
|
|
|
@ -13,6 +13,9 @@ import { FtrProviderContext } from '../ftr_provider_context';
|
|||
const SEARCH_SESSION_INDICATOR_TEST_SUBJ = 'searchSessionIndicator';
|
||||
const SEARCH_SESSIONS_POPOVER_CONTENT_TEST_SUBJ = 'searchSessionIndicatorPopoverContainer';
|
||||
|
||||
export const TOUR_TAKING_TOO_LONG_STEP_KEY = `data.searchSession.tour.takingTooLong`;
|
||||
export const TOUR_RESTORE_STEP_KEY = `data.searchSession.tour.restore`;
|
||||
|
||||
type SessionStateType =
|
||||
| 'none'
|
||||
| 'loading'
|
||||
|
@ -61,7 +64,7 @@ export function SearchSessionsProvider({ getService }: FtrProviderContext) {
|
|||
public async viewSearchSessions() {
|
||||
log.debug('viewSearchSessions');
|
||||
await this.ensurePopoverOpened();
|
||||
await testSubjects.click('searchSessionIndicatorviewSearchSessionsLink');
|
||||
await testSubjects.click('searchSessionIndicatorViewSearchSessionsLink');
|
||||
}
|
||||
|
||||
public async save() {
|
||||
|
@ -78,17 +81,22 @@ export function SearchSessionsProvider({ getService }: FtrProviderContext) {
|
|||
await this.ensurePopoverClosed();
|
||||
}
|
||||
|
||||
public async refresh() {
|
||||
log.debug('refresh the status');
|
||||
await this.ensurePopoverOpened();
|
||||
await testSubjects.click('searchSessionIndicatorRefreshBtn');
|
||||
await this.ensurePopoverClosed();
|
||||
}
|
||||
|
||||
public async openPopover() {
|
||||
await this.ensurePopoverOpened();
|
||||
}
|
||||
|
||||
public async openedOrFail() {
|
||||
return testSubjects.existOrFail(SEARCH_SESSIONS_POPOVER_CONTENT_TEST_SUBJ, {
|
||||
timeout: 15000, // because popover auto opens after search takes 10s
|
||||
});
|
||||
}
|
||||
|
||||
public async closedOrFail() {
|
||||
return testSubjects.missingOrFail(SEARCH_SESSIONS_POPOVER_CONTENT_TEST_SUBJ, {
|
||||
timeout: 15000, // because popover auto opens after search takes 10s
|
||||
});
|
||||
}
|
||||
|
||||
private async ensurePopoverOpened() {
|
||||
log.debug('ensurePopoverOpened');
|
||||
const isAlreadyOpen = await testSubjects.exists(SEARCH_SESSIONS_POPOVER_CONTENT_TEST_SUBJ);
|
||||
|
@ -143,5 +151,19 @@ export function SearchSessionsProvider({ getService }: FtrProviderContext) {
|
|||
);
|
||||
});
|
||||
}
|
||||
|
||||
public async markTourDone() {
|
||||
await Promise.all([
|
||||
browser.setLocalStorageItem(TOUR_TAKING_TOO_LONG_STEP_KEY, 'true'),
|
||||
browser.setLocalStorageItem(TOUR_RESTORE_STEP_KEY, 'true'),
|
||||
]);
|
||||
}
|
||||
|
||||
public async markTourUndone() {
|
||||
await Promise.all([
|
||||
browser.removeLocalStorageItem(TOUR_TAKING_TOO_LONG_STEP_KEY),
|
||||
browser.removeLocalStorageItem(TOUR_RESTORE_STEP_KEY),
|
||||
]);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
|
|
@ -7,9 +7,11 @@
|
|||
|
||||
import { FtrProviderContext } from '../../../../ftr_provider_context';
|
||||
|
||||
export default function ({ loadTestFile, getService }: FtrProviderContext) {
|
||||
export default function ({ loadTestFile, getService, getPageObjects }: FtrProviderContext) {
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const PageObjects = getPageObjects(['common']);
|
||||
const searchSessions = getService('searchSessions');
|
||||
|
||||
describe('async search', function () {
|
||||
this.tags('ciGroup3');
|
||||
|
@ -19,6 +21,11 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) {
|
|||
await esArchiver.load('dashboard/async_search');
|
||||
await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' });
|
||||
await kibanaServer.uiSettings.replace({ 'search:timeout': 10000 });
|
||||
await PageObjects.common.navigateToApp('dashboard');
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await searchSessions.markTourDone();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
|
@ -28,6 +35,7 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./async_search'));
|
||||
loadTestFile(require.resolve('./send_to_background'));
|
||||
loadTestFile(require.resolve('./send_to_background_relative_time'));
|
||||
loadTestFile(require.resolve('./search_sessions_tour'));
|
||||
loadTestFile(require.resolve('./sessions_in_space'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { FtrProviderContext } from '../../../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const es = getService('es');
|
||||
const log = getService('log');
|
||||
const PageObjects = getPageObjects(['common', 'header', 'dashboard', 'visChart']);
|
||||
const browser = getService('browser');
|
||||
const searchSessions = getService('searchSessions');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
|
||||
describe('search sessions tour', () => {
|
||||
before(async function () {
|
||||
const { body } = await es.info();
|
||||
if (!body.version.number.includes('SNAPSHOT')) {
|
||||
log.debug('Skipping because this build does not have the required shard_delay agg');
|
||||
this.skip();
|
||||
return;
|
||||
}
|
||||
await kibanaServer.uiSettings.replace({ 'search:timeout': 30000 });
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await PageObjects.common.navigateToApp('dashboard');
|
||||
await searchSessions.markTourUndone();
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await searchSessions.deleteAllSearchSessions();
|
||||
await kibanaServer.uiSettings.replace({ 'search:timeout': 10000 });
|
||||
await searchSessions.markTourDone();
|
||||
});
|
||||
|
||||
it('search session popover auto opens when search is taking a while', async () => {
|
||||
await PageObjects.dashboard.loadSavedDashboard('Delayed 15s');
|
||||
|
||||
await searchSessions.openedOrFail(); // tour auto opens when there is a long running search
|
||||
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await searchSessions.expectState('completed');
|
||||
|
||||
const url = await browser.getCurrentUrl();
|
||||
const fakeSessionId = '__fake__';
|
||||
const savedSessionURL = `${url}&searchSessionId=${fakeSessionId}`;
|
||||
await browser.get(savedSessionURL);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await searchSessions.expectState('restored');
|
||||
await searchSessions.openedOrFail(); // tour auto opens on first restore
|
||||
|
||||
await browser.get(savedSessionURL);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await searchSessions.expectState('restored');
|
||||
await searchSessions.closedOrFail(); // do not open on next restore
|
||||
});
|
||||
});
|
||||
}
|
|
@ -16,6 +16,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
const dashboardPanelActions = getService('dashboardPanelActions');
|
||||
const browser = getService('browser');
|
||||
const searchSessions = getService('searchSessions');
|
||||
const queryBar = getService('queryBar');
|
||||
|
||||
describe('send to background', () => {
|
||||
before(async function () {
|
||||
|
@ -46,7 +47,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
);
|
||||
expect(session1).to.be(fakeSessionId);
|
||||
|
||||
await searchSessions.refresh();
|
||||
await queryBar.clickQuerySubmitButton();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await searchSessions.expectState('completed');
|
||||
await testSubjects.missingOrFail('embeddableErrorLabel');
|
||||
|
@ -65,6 +66,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
expect(url).to.contain('searchSessionId');
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await searchSessions.expectState('restored');
|
||||
|
||||
expect(
|
||||
await dashboardPanelActions.getSearchSessionIdByTitle('Sum of Bytes by Extension')
|
||||
).to.be(fakeSessionId);
|
||||
|
|
|
@ -7,9 +7,11 @@
|
|||
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default function ({ loadTestFile, getService }: FtrProviderContext) {
|
||||
export default function ({ loadTestFile, getService, getPageObjects }: FtrProviderContext) {
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const PageObjects = getPageObjects(['common']);
|
||||
const searchSessions = getService('searchSessions');
|
||||
|
||||
describe('async search', function () {
|
||||
this.tags('ciGroup3');
|
||||
|
@ -17,6 +19,11 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) {
|
|||
before(async () => {
|
||||
await esArchiver.loadIfNeeded('logstash_functional');
|
||||
await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' });
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await searchSessions.markTourDone();
|
||||
});
|
||||
|
||||
loadTestFile(require.resolve('./async_search'));
|
||||
|
|
|
@ -31,6 +31,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
retry.tryForTime(10000, async () => {
|
||||
testSubjects.existOrFail('dashboardLandingPage');
|
||||
});
|
||||
await searchSessions.markTourDone();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue