[Search Session] Control "Kibana / Search Sessions" management section by privileges (#90818)
This commit is contained in:
parent
c91e5fe3f2
commit
9ab5bcb141
22 changed files with 370 additions and 71 deletions
|
@ -126,6 +126,7 @@
|
|||
| [noSearchSessionStorageCapabilityMessage](./kibana-plugin-plugins-data-public.nosearchsessionstoragecapabilitymessage.md) | Message to display in case storing session session is disabled due to turned off capability |
|
||||
| [parseSearchSourceJSON](./kibana-plugin-plugins-data-public.parsesearchsourcejson.md) | |
|
||||
| [QueryStringInput](./kibana-plugin-plugins-data-public.querystringinput.md) | |
|
||||
| [SEARCH\_SESSIONS\_MANAGEMENT\_ID](./kibana-plugin-plugins-data-public.search_sessions_management_id.md) | |
|
||||
| [search](./kibana-plugin-plugins-data-public.search.md) | |
|
||||
| [SearchBar](./kibana-plugin-plugins-data-public.searchbar.md) | |
|
||||
| [syncQueryStateWithUrl](./kibana-plugin-plugins-data-public.syncquerystatewithurl.md) | Helper to setup syncing of global data with the URL |
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SEARCH\_SESSIONS\_MANAGEMENT\_ID](./kibana-plugin-plugins-data-public.search_sessions_management_id.md)
|
||||
|
||||
## SEARCH\_SESSIONS\_MANAGEMENT\_ID variable
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
SEARCH_SESSIONS_MANAGEMENT_ID = "search_sessions"
|
||||
```
|
|
@ -381,6 +381,7 @@ export {
|
|||
TimeoutErrorMode,
|
||||
PainlessError,
|
||||
noSearchSessionStorageCapabilityMessage,
|
||||
SEARCH_SESSIONS_MANAGEMENT_ID,
|
||||
} from './search';
|
||||
|
||||
export type {
|
||||
|
|
|
@ -2238,6 +2238,11 @@ export const search: {
|
|||
tabifyGetColumns: typeof tabifyGetColumns;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "SEARCH_SESSIONS_MANAGEMENT_ID" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const SEARCH_SESSIONS_MANAGEMENT_ID = "search_sessions";
|
||||
|
||||
// Warning: (ae-missing-release-tag) "SearchBar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
|
@ -2601,23 +2606,23 @@ export const UI_SETTINGS: {
|
|||
// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:396:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:396:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:396:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:396:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:398:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:408:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:409:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:410:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:415:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:416:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:419:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:420:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:397:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:397:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:397:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:397:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:409:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:410:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:416:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:417:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:420:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:421:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/index.ts:424:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:34:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/search/session/session_service.ts:41:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts
|
||||
// src/plugins/data/public/search/session/session_service.ts:42:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ export {
|
|||
SessionsClient,
|
||||
ISessionsClient,
|
||||
noSearchSessionStorageCapabilityMessage,
|
||||
SEARCH_SESSIONS_MANAGEMENT_ID,
|
||||
} from './session';
|
||||
export { getEsPreference } from './es_search';
|
||||
|
||||
|
|
|
@ -95,21 +95,23 @@ describe('SearchInterceptor', () => {
|
|||
});
|
||||
|
||||
describe('Search session', () => {
|
||||
const setup = ({
|
||||
isRestore = false,
|
||||
isStored = false,
|
||||
sessionId,
|
||||
}: {
|
||||
isRestore?: boolean;
|
||||
isStored?: boolean;
|
||||
sessionId: string;
|
||||
}) => {
|
||||
const setup = (
|
||||
opts: {
|
||||
isRestore?: boolean;
|
||||
isStored?: boolean;
|
||||
sessionId: string;
|
||||
} | null
|
||||
) => {
|
||||
const sessionServiceMock = searchMock.session as jest.Mocked<ISessionService>;
|
||||
sessionServiceMock.getSearchOptions.mockImplementation(() => ({
|
||||
sessionId,
|
||||
isRestore,
|
||||
isStored,
|
||||
}));
|
||||
sessionServiceMock.getSearchOptions.mockImplementation(() =>
|
||||
opts
|
||||
? {
|
||||
sessionId: opts.sessionId,
|
||||
isRestore: opts.isRestore ?? false,
|
||||
isStored: opts.isStored ?? false,
|
||||
}
|
||||
: null
|
||||
);
|
||||
fetchMock.mockResolvedValue({ result: 200 });
|
||||
};
|
||||
|
||||
|
@ -142,6 +144,22 @@ describe('SearchInterceptor', () => {
|
|||
(searchMock.session as jest.Mocked<ISessionService>).getSearchOptions
|
||||
).toHaveBeenCalledWith(sessionId);
|
||||
});
|
||||
|
||||
test("doesn't forward sessionId if search options return null", async () => {
|
||||
const sessionId = 'sid';
|
||||
setup(null);
|
||||
|
||||
await searchInterceptor.search(mockRequest, { sessionId }).toPromise();
|
||||
expect(fetchMock.mock.calls[0][0]).toEqual(
|
||||
expect.not.objectContaining({
|
||||
options: { sessionId },
|
||||
})
|
||||
);
|
||||
|
||||
expect(
|
||||
(searchMock.session as jest.Mocked<ISessionService>).getSearchOptions
|
||||
).toHaveBeenCalledWith(sessionId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Should throw typed errors', () => {
|
||||
|
|
|
@ -126,14 +126,14 @@ export class SearchInterceptor {
|
|||
request: IKibanaSearchRequest,
|
||||
options?: ISearchOptions
|
||||
): Promise<IKibanaSearchResponse> {
|
||||
const { abortSignal, ...requestOptions } = options || {};
|
||||
const { abortSignal, sessionId, ...requestOptions } = options || {};
|
||||
|
||||
return this.batchedFetch(
|
||||
{
|
||||
request,
|
||||
options: {
|
||||
...requestOptions,
|
||||
...(options?.sessionId && this.deps.session.getSearchOptions(options.sessionId)),
|
||||
...this.deps.session.getSearchOptions(sessionId),
|
||||
},
|
||||
},
|
||||
abortSignal
|
||||
|
|
9
src/plugins/data/public/search/session/constants.ts
Normal file
9
src/plugins/data/public/search/session/constants.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export const SEARCH_SESSIONS_MANAGEMENT_ID = 'search_sessions';
|
|
@ -10,3 +10,4 @@ export { SessionService, ISessionService, SearchSessionInfoProvider } from './se
|
|||
export { SearchSessionState } from './search_session_state';
|
||||
export { SessionsClient, ISessionsClient } from './sessions_client';
|
||||
export { noSearchSessionStorageCapabilityMessage } from './i18n';
|
||||
export { SEARCH_SESSIONS_MANAGEMENT_ID } from './constants';
|
||||
|
|
|
@ -41,5 +41,6 @@ export function getSessionServiceMock(): jest.Mocked<ISessionService> {
|
|||
enableStorage: jest.fn(),
|
||||
isSessionStorageReady: jest.fn(() => true),
|
||||
getSearchSessionIndicatorUiConfig: jest.fn(() => ({ isDisabled: () => ({ disabled: false }) })),
|
||||
hasAccess: jest.fn(() => true),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -14,11 +14,13 @@ import { BehaviorSubject } from 'rxjs';
|
|||
import { SearchSessionState } from './search_session_state';
|
||||
import { createNowProviderMock } from '../../now_provider/mocks';
|
||||
import { NowProviderInternalContract } from '../../now_provider';
|
||||
import { SEARCH_SESSIONS_MANAGEMENT_ID } from './constants';
|
||||
|
||||
describe('Session service', () => {
|
||||
let sessionService: ISessionService;
|
||||
let state$: BehaviorSubject<SearchSessionState>;
|
||||
let nowProvider: jest.Mocked<NowProviderInternalContract>;
|
||||
let userHasAccessToSearchSessions = true;
|
||||
|
||||
beforeEach(() => {
|
||||
const initializerContext = coreMock.createPluginInitializerContext();
|
||||
|
@ -30,7 +32,18 @@ describe('Session service', () => {
|
|||
startService().then(([coreStart, ...rest]) => [
|
||||
{
|
||||
...coreStart,
|
||||
application: { ...coreStart.application, currentAppId$: new BehaviorSubject('app') },
|
||||
application: {
|
||||
...coreStart.application,
|
||||
currentAppId$: new BehaviorSubject('app'),
|
||||
capabilities: {
|
||||
...coreStart.application.capabilities,
|
||||
management: {
|
||||
kibana: {
|
||||
[SEARCH_SESSIONS_MANAGEMENT_ID]: userHasAccessToSearchSessions,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
...rest,
|
||||
]),
|
||||
|
@ -146,6 +159,8 @@ describe('Session service', () => {
|
|||
isRestore: true,
|
||||
sessionId,
|
||||
});
|
||||
|
||||
expect(sessionService.getSearchOptions(undefined)).toBeNull();
|
||||
});
|
||||
test('isCurrentSession', () => {
|
||||
expect(sessionService.isCurrentSession()).toBeFalsy();
|
||||
|
@ -214,4 +229,25 @@ describe('Session service', () => {
|
|||
sessionService.start();
|
||||
await expect(() => sessionService.save()).rejects.toMatchInlineSnapshot(`[Error: Haha]`);
|
||||
});
|
||||
|
||||
describe("user doesn't have access to search session", () => {
|
||||
beforeAll(() => {
|
||||
userHasAccessToSearchSessions = false;
|
||||
});
|
||||
afterAll(() => {
|
||||
userHasAccessToSearchSessions = true;
|
||||
});
|
||||
|
||||
test("getSearchOptions doesn't return sessionId", () => {
|
||||
const sessionId = sessionService.start();
|
||||
expect(sessionService.getSearchOptions(sessionId)).toBeNull();
|
||||
});
|
||||
|
||||
test('save() throws', async () => {
|
||||
sessionService.start();
|
||||
await expect(() => sessionService.save()).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"No access to search sessions"`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
import { ISessionsClient } from './sessions_client';
|
||||
import { ISearchOptions } from '../../../common';
|
||||
import { NowProviderInternalContract } from '../../now_provider';
|
||||
import { SEARCH_SESSIONS_MANAGEMENT_ID } from './constants';
|
||||
|
||||
export type ISessionService = PublicContract<SessionService>;
|
||||
|
||||
|
@ -68,6 +69,7 @@ export class SessionService {
|
|||
private searchSessionIndicatorUiConfig?: Partial<SearchSessionIndicatorUiConfig>;
|
||||
private subscription = new Subscription();
|
||||
private curApp?: string;
|
||||
private hasAccessToSearchSessions: boolean = false;
|
||||
|
||||
constructor(
|
||||
initializerContext: PluginInitializerContext<ConfigSchema>,
|
||||
|
@ -94,6 +96,10 @@ export class SessionService {
|
|||
);
|
||||
|
||||
getStartServices().then(([coreStart]) => {
|
||||
// using management?.kibana? we infer if any of the apps allows current user to store sessions
|
||||
this.hasAccessToSearchSessions =
|
||||
coreStart.application.capabilities.management?.kibana?.[SEARCH_SESSIONS_MANAGEMENT_ID];
|
||||
|
||||
// Apps required to clean up their sessions before unmounting
|
||||
// Make sure that apps don't leave sessions open.
|
||||
this.subscription.add(
|
||||
|
@ -117,6 +123,15 @@ export class SessionService {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* If user has access to search sessions
|
||||
* This resolves to `true` in case at least one app allows user to create search session
|
||||
* In this case search session management is available
|
||||
*/
|
||||
public hasAccess() {
|
||||
return this.hasAccessToSearchSessions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to track pending searches within current session
|
||||
*
|
||||
|
@ -215,6 +230,7 @@ export class SessionService {
|
|||
const sessionId = this.getSessionId();
|
||||
if (!sessionId) throw new Error('No current session');
|
||||
if (!this.curApp) throw new Error('No current app id');
|
||||
if (!this.hasAccess()) throw new Error('No access to search sessions');
|
||||
const currentSessionInfoProvider = this.searchSessionInfoProvider;
|
||||
if (!currentSessionInfoProvider) throw new Error('No info provider for current session');
|
||||
const [name, { initialState, restoreState, urlGeneratorId }] = await Promise.all([
|
||||
|
@ -247,11 +263,25 @@ export class SessionService {
|
|||
|
||||
/**
|
||||
* Infers search session options for sessionId using current session state
|
||||
*
|
||||
* In case user doesn't has access to `search-session` SO returns null,
|
||||
* meaning that sessionId and other session parameters shouldn't be used when doing searches
|
||||
*
|
||||
* @param sessionId
|
||||
*/
|
||||
public getSearchOptions(
|
||||
sessionId: string
|
||||
): Required<Pick<ISearchOptions, 'sessionId' | 'isRestore' | 'isStored'>> {
|
||||
sessionId?: string
|
||||
): Required<Pick<ISearchOptions, 'sessionId' | 'isRestore' | 'isStored'>> | null {
|
||||
if (!sessionId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// in case user doesn't have permissions to search session, do not forward sessionId to the server
|
||||
// because user most likely also doesn't have access to `search-session` SO
|
||||
if (!this.hasAccessToSearchSessions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isCurrentSession = this.isCurrentSession(sessionId);
|
||||
return {
|
||||
sessionId,
|
||||
|
|
|
@ -94,6 +94,7 @@ export function getTimelionRequestHandler({
|
|||
});
|
||||
|
||||
try {
|
||||
const searchSessionOptions = dataSearch.session.getSearchOptions(searchSessionId);
|
||||
return await http.post('/api/timelion/run', {
|
||||
body: JSON.stringify({
|
||||
sheet: [expression],
|
||||
|
@ -108,8 +109,8 @@ export function getTimelionRequestHandler({
|
|||
interval: visParams.interval,
|
||||
timezone,
|
||||
},
|
||||
...(searchSessionId && {
|
||||
searchSession: dataSearch.session.getSearchOptions(searchSessionId),
|
||||
...(searchSessionOptions && {
|
||||
searchSession: searchSessionOptions,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -48,6 +48,7 @@ export const metricsRequestHandler = async ({
|
|||
});
|
||||
|
||||
try {
|
||||
const searchSessionOptions = dataSearch.session.getSearchOptions(searchSessionId);
|
||||
return await getCoreStart().http.post(ROUTES.VIS_DATA, {
|
||||
body: JSON.stringify({
|
||||
timerange: {
|
||||
|
@ -58,8 +59,8 @@ export const metricsRequestHandler = async ({
|
|||
filters: input?.filters,
|
||||
panels: [visParams],
|
||||
state: uiStateObj,
|
||||
...(searchSessionId && {
|
||||
searchSession: dataSearch.session.getSearchOptions(searchSessionId),
|
||||
...(searchSessionOptions && {
|
||||
searchSession: searchSessionOptions,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -15,6 +15,7 @@ import type { ConfigSchema } from '../../../config';
|
|||
import type { DataEnhancedStartDependencies } from '../../plugin';
|
||||
import type { SearchSessionsMgmtAPI } from './lib/api';
|
||||
import type { AsyncSearchIntroDocumentation } from './lib/documentation';
|
||||
import { SEARCH_SESSIONS_MANAGEMENT_ID } from '../../../../../../src/plugins/data/public';
|
||||
|
||||
export interface IManagementSectionsPluginsSetup {
|
||||
management: ManagementSetup;
|
||||
|
@ -38,7 +39,7 @@ export interface AppDependencies {
|
|||
}
|
||||
|
||||
export const APP = {
|
||||
id: 'search_sessions',
|
||||
id: SEARCH_SESSIONS_MANAGEMENT_ID,
|
||||
getI18nName: (): string =>
|
||||
i18n.translate('xpack.data.mgmt.searchSessions.appTitle', {
|
||||
defaultMessage: 'Search Sessions',
|
||||
|
|
|
@ -24,6 +24,7 @@ import userEvent from '@testing-library/user-event';
|
|||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
const coreStart = coreMock.createStart();
|
||||
const application = coreStart.application;
|
||||
const dataStart = dataPluginMock.createStartContract();
|
||||
const sessionService = dataStart.search.session as jest.Mocked<ISessionService>;
|
||||
let storage: Storage;
|
||||
|
@ -52,7 +53,7 @@ beforeEach(() => {
|
|||
test("shouldn't show indicator in case no active search session", async () => {
|
||||
const SearchSessionIndicator = createConnectedSearchSessionIndicator({
|
||||
sessionService,
|
||||
application: coreStart.application,
|
||||
application,
|
||||
timeFilter,
|
||||
storage,
|
||||
disableSaveAfterSessionCompletesTimeout,
|
||||
|
@ -79,7 +80,7 @@ test("shouldn't show indicator in case no active search session", async () => {
|
|||
test("shouldn't show indicator in case app hasn't opt-in", async () => {
|
||||
const SearchSessionIndicator = createConnectedSearchSessionIndicator({
|
||||
sessionService,
|
||||
application: coreStart.application,
|
||||
application,
|
||||
timeFilter,
|
||||
storage,
|
||||
disableSaveAfterSessionCompletesTimeout,
|
||||
|
@ -108,7 +109,7 @@ test('should show indicator in case there is an active search session', async ()
|
|||
const state$ = new BehaviorSubject(SearchSessionState.Loading);
|
||||
const SearchSessionIndicator = createConnectedSearchSessionIndicator({
|
||||
sessionService: { ...sessionService, state$ },
|
||||
application: coreStart.application,
|
||||
application,
|
||||
timeFilter,
|
||||
storage,
|
||||
disableSaveAfterSessionCompletesTimeout,
|
||||
|
@ -124,12 +125,6 @@ test('should show indicator in case there is an active search session', async ()
|
|||
|
||||
test('should be disabled in case uiConfig says so ', async () => {
|
||||
const state$ = new BehaviorSubject(SearchSessionState.Loading);
|
||||
coreStart.application.currentAppId$ = new BehaviorSubject('discover');
|
||||
(coreStart.application.capabilities as any) = {
|
||||
discover: {
|
||||
storeSearchSession: false,
|
||||
},
|
||||
};
|
||||
sessionService.getSearchSessionIndicatorUiConfig.mockImplementation(() => ({
|
||||
isDisabled: () => ({
|
||||
disabled: true,
|
||||
|
@ -138,7 +133,7 @@ test('should be disabled in case uiConfig says so ', async () => {
|
|||
}));
|
||||
const SearchSessionIndicator = createConnectedSearchSessionIndicator({
|
||||
sessionService: { ...sessionService, state$ },
|
||||
application: coreStart.application,
|
||||
application,
|
||||
timeFilter,
|
||||
storage,
|
||||
disableSaveAfterSessionCompletesTimeout,
|
||||
|
@ -157,12 +152,36 @@ test('should be disabled in case uiConfig says so ', async () => {
|
|||
expect(screen.getByRole('button', { name: 'Save session' })).toBeDisabled();
|
||||
});
|
||||
|
||||
test('should be disabled in case not enough permissions', async () => {
|
||||
const state$ = new BehaviorSubject(SearchSessionState.Completed);
|
||||
const SearchSessionIndicator = createConnectedSearchSessionIndicator({
|
||||
sessionService: { ...sessionService, state$, hasAccess: () => false },
|
||||
application,
|
||||
timeFilter,
|
||||
storage,
|
||||
disableSaveAfterSessionCompletesTimeout,
|
||||
});
|
||||
|
||||
render(
|
||||
<Container>
|
||||
<SearchSessionIndicator />
|
||||
</Container>
|
||||
);
|
||||
|
||||
await waitFor(() => screen.getByTestId('searchSessionIndicator'));
|
||||
|
||||
await userEvent.click(screen.getByLabelText('Search session complete'));
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Save session' })).toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: 'Manage sessions' })).toBeDisabled();
|
||||
});
|
||||
|
||||
test('should be disabled during auto-refresh', async () => {
|
||||
const state$ = new BehaviorSubject(SearchSessionState.Loading);
|
||||
|
||||
const SearchSessionIndicator = createConnectedSearchSessionIndicator({
|
||||
sessionService: { ...sessionService, state$ },
|
||||
application: coreStart.application,
|
||||
application,
|
||||
timeFilter,
|
||||
storage,
|
||||
disableSaveAfterSessionCompletesTimeout,
|
||||
|
@ -199,7 +218,7 @@ describe('Completed inactivity', () => {
|
|||
|
||||
const SearchSessionIndicator = createConnectedSearchSessionIndicator({
|
||||
sessionService: { ...sessionService, state$ },
|
||||
application: coreStart.application,
|
||||
application,
|
||||
timeFilter,
|
||||
storage,
|
||||
disableSaveAfterSessionCompletesTimeout,
|
||||
|
@ -257,7 +276,7 @@ describe('tour steps', () => {
|
|||
const state$ = new BehaviorSubject(SearchSessionState.Loading);
|
||||
const SearchSessionIndicator = createConnectedSearchSessionIndicator({
|
||||
sessionService: { ...sessionService, state$ },
|
||||
application: coreStart.application,
|
||||
application,
|
||||
timeFilter,
|
||||
storage,
|
||||
disableSaveAfterSessionCompletesTimeout,
|
||||
|
@ -294,7 +313,7 @@ describe('tour steps', () => {
|
|||
const state$ = new BehaviorSubject(SearchSessionState.Loading);
|
||||
const SearchSessionIndicator = createConnectedSearchSessionIndicator({
|
||||
sessionService: { ...sessionService, state$ },
|
||||
application: coreStart.application,
|
||||
application,
|
||||
timeFilter,
|
||||
storage,
|
||||
disableSaveAfterSessionCompletesTimeout,
|
||||
|
@ -325,7 +344,7 @@ describe('tour steps', () => {
|
|||
const state$ = new BehaviorSubject(SearchSessionState.Restored);
|
||||
const SearchSessionIndicator = createConnectedSearchSessionIndicator({
|
||||
sessionService: { ...sessionService, state$ },
|
||||
application: coreStart.application,
|
||||
application,
|
||||
timeFilter,
|
||||
storage,
|
||||
disableSaveAfterSessionCompletesTimeout,
|
||||
|
@ -347,7 +366,7 @@ describe('tour steps', () => {
|
|||
const state$ = new BehaviorSubject(SearchSessionState.Completed);
|
||||
const SearchSessionIndicator = createConnectedSearchSessionIndicator({
|
||||
sessionService: { ...sessionService, state$ },
|
||||
application: coreStart.application,
|
||||
application,
|
||||
timeFilter,
|
||||
storage,
|
||||
disableSaveAfterSessionCompletesTimeout,
|
||||
|
|
|
@ -79,6 +79,9 @@ export const createConnectedSearchSessionIndicator = ({
|
|||
let saveDisabled = false;
|
||||
let saveDisabledReasonText: string = '';
|
||||
|
||||
let managementDisabled = false;
|
||||
let managementDisabledReasonText: string = '';
|
||||
|
||||
if (autoRefreshEnabled) {
|
||||
saveDisabled = true;
|
||||
saveDisabledReasonText = i18n.translate(
|
||||
|
@ -104,6 +107,18 @@ export const createConnectedSearchSessionIndicator = ({
|
|||
saveDisabledReasonText = isSaveDisabledByApp.reasonText;
|
||||
}
|
||||
|
||||
// check if user doesn't have access to search_sessions and search_sessions mgtm
|
||||
// this happens in case there is no app that allows current user to use search session
|
||||
if (!sessionService.hasAccess()) {
|
||||
managementDisabled = saveDisabled = true;
|
||||
managementDisabledReasonText = saveDisabledReasonText = i18n.translate(
|
||||
'xpack.data.searchSessionIndicator.disabledDueToDisabledGloballyMessage',
|
||||
{
|
||||
defaultMessage: "You don't have permissions to manage search sessions",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const { markOpenedDone, markRestoredDone } = useSearchSessionTour(
|
||||
storage,
|
||||
searchSessionIndicator,
|
||||
|
@ -143,6 +158,8 @@ export const createConnectedSearchSessionIndicator = ({
|
|||
state={state}
|
||||
saveDisabled={saveDisabled}
|
||||
saveDisabledReasonText={saveDisabledReasonText}
|
||||
managementDisabled={managementDisabled}
|
||||
managementDisabledReasonText={managementDisabledReasonText}
|
||||
onContinueInBackground={onContinueInBackground}
|
||||
onSaveResults={onSaveResults}
|
||||
onCancel={onCancel}
|
||||
|
|
|
@ -31,7 +31,8 @@ export interface SearchSessionIndicatorProps {
|
|||
onCancel?: () => void;
|
||||
viewSearchSessionsLink?: string;
|
||||
onSaveResults?: () => void;
|
||||
|
||||
managementDisabled?: boolean;
|
||||
managementDisabledReasonText?: string;
|
||||
saveDisabled?: boolean;
|
||||
saveDisabledReasonText?: string;
|
||||
|
||||
|
@ -78,17 +79,22 @@ const ContinueInBackgroundButton = ({
|
|||
const ViewAllSearchSessionsButton = ({
|
||||
viewSearchSessionsLink = 'management/kibana/search_sessions',
|
||||
buttonProps = {},
|
||||
managementDisabled,
|
||||
managementDisabledReasonText,
|
||||
}: ActionButtonProps) => (
|
||||
<EuiButtonEmpty
|
||||
href={viewSearchSessionsLink}
|
||||
data-test-subj={'searchSessionIndicatorViewSearchSessionsLink'}
|
||||
{...buttonProps}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.data.searchSessionIndicator.viewSearchSessionsLinkText"
|
||||
defaultMessage="Manage sessions"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
<EuiToolTip content={managementDisabledReasonText}>
|
||||
<EuiButtonEmpty
|
||||
href={viewSearchSessionsLink}
|
||||
data-test-subj={'searchSessionIndicatorViewSearchSessionsLink'}
|
||||
isDisabled={managementDisabled}
|
||||
{...buttonProps}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.data.searchSessionIndicator.viewSearchSessionsLinkText"
|
||||
defaultMessage="Manage sessions"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiToolTip>
|
||||
);
|
||||
|
||||
const SaveButton = ({
|
||||
|
|
|
@ -67,7 +67,11 @@ Array [
|
|||
"catalogue": Array [
|
||||
"dashboard",
|
||||
],
|
||||
"management": Object {},
|
||||
"management": Object {
|
||||
"kibana": Array [
|
||||
"search_sessions",
|
||||
],
|
||||
},
|
||||
"savedObject": Object {
|
||||
"all": Array [
|
||||
"dashboard",
|
||||
|
@ -200,7 +204,11 @@ Array [
|
|||
"catalogue": Array [
|
||||
"discover",
|
||||
],
|
||||
"management": Object {},
|
||||
"management": Object {
|
||||
"kibana": Array [
|
||||
"search_sessions",
|
||||
],
|
||||
},
|
||||
"savedObject": Object {
|
||||
"all": Array [
|
||||
"search",
|
||||
|
@ -553,7 +561,11 @@ Array [
|
|||
"catalogue": Array [
|
||||
"dashboard",
|
||||
],
|
||||
"management": Object {},
|
||||
"management": Object {
|
||||
"kibana": Array [
|
||||
"search_sessions",
|
||||
],
|
||||
},
|
||||
"savedObject": Object {
|
||||
"all": Array [
|
||||
"dashboard",
|
||||
|
@ -686,7 +698,11 @@ Array [
|
|||
"catalogue": Array [
|
||||
"discover",
|
||||
],
|
||||
"management": Object {},
|
||||
"management": Object {
|
||||
"kibana": Array [
|
||||
"search_sessions",
|
||||
],
|
||||
},
|
||||
"savedObject": Object {
|
||||
"all": Array [
|
||||
"search",
|
||||
|
|
|
@ -21,6 +21,9 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
|
|||
name: i18n.translate('xpack.features.discoverFeatureName', {
|
||||
defaultMessage: 'Discover',
|
||||
}),
|
||||
management: {
|
||||
kibana: ['search_sessions'],
|
||||
},
|
||||
order: 100,
|
||||
category: DEFAULT_APP_CATEGORIES.kibana,
|
||||
app: ['discover', 'kibana'],
|
||||
|
@ -95,6 +98,9 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
|
|||
read: [],
|
||||
},
|
||||
ui: ['storeSearchSession'],
|
||||
management: {
|
||||
kibana: ['search_sessions'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -166,6 +172,9 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
|
|||
name: i18n.translate('xpack.features.dashboardFeatureName', {
|
||||
defaultMessage: 'Dashboard',
|
||||
}),
|
||||
management: {
|
||||
kibana: ['search_sessions'],
|
||||
},
|
||||
order: 200,
|
||||
category: DEFAULT_APP_CATEGORIES.kibana,
|
||||
app: ['dashboards', 'kibana'],
|
||||
|
@ -260,6 +269,9 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS
|
|||
read: [],
|
||||
},
|
||||
ui: ['storeSearchSession'],
|
||||
management: {
|
||||
kibana: ['search_sessions'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -22,5 +22,6 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
loadTestFile(require.resolve('./sessions_management'));
|
||||
loadTestFile(require.resolve('./sessions_management_permissions'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
const security = getService('security');
|
||||
const PageObjects = getPageObjects([
|
||||
'common',
|
||||
'header',
|
||||
'dashboard',
|
||||
'visChart',
|
||||
'searchSessionsManagement',
|
||||
'security',
|
||||
]);
|
||||
|
||||
const appsMenu = getService('appsMenu');
|
||||
const managementMenu = getService('managementMenu');
|
||||
|
||||
describe('Search sessions Management UI permissions', () => {
|
||||
describe('Sessions management is not available if non of apps enable search sessions', () => {
|
||||
before(async () => {
|
||||
await security.role.create('data_analyst', {
|
||||
elasticsearch: {},
|
||||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
dashboard: ['read'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await security.user.create('analyst', {
|
||||
password: 'analyst-password',
|
||||
roles: ['data_analyst'],
|
||||
full_name: 'test user',
|
||||
});
|
||||
|
||||
await PageObjects.security.forceLogout();
|
||||
|
||||
await PageObjects.security.login('analyst', 'analyst-password', {
|
||||
expectSpaceSelector: false,
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await security.role.delete('data_analyst');
|
||||
await security.user.delete('analyst');
|
||||
await PageObjects.security.forceLogout();
|
||||
});
|
||||
|
||||
it('Sessions management is not available if non of apps enable search sessions', async () => {
|
||||
const links = await appsMenu.readLinks();
|
||||
expect(links.map((link) => link.text)).to.not.contain('Stack Management');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Sessions management is available if one of apps enables search sessions', () => {
|
||||
before(async () => {
|
||||
await security.role.create('data_analyst', {
|
||||
elasticsearch: {},
|
||||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
dashboard: ['read', 'store_search_session'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await security.user.create('analyst', {
|
||||
password: 'analyst-password',
|
||||
roles: ['data_analyst'],
|
||||
full_name: 'test user',
|
||||
});
|
||||
|
||||
await PageObjects.security.forceLogout();
|
||||
|
||||
await PageObjects.security.login('analyst', 'analyst-password', {
|
||||
expectSpaceSelector: false,
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await security.role.delete('data_analyst');
|
||||
await security.user.delete('analyst');
|
||||
await PageObjects.security.forceLogout();
|
||||
});
|
||||
|
||||
it('Sessions management is available if one of apps enables search sessions', async () => {
|
||||
const links = await appsMenu.readLinks();
|
||||
expect(links.map((link) => link.text)).to.contain('Stack Management');
|
||||
await PageObjects.common.navigateToApp('management');
|
||||
const sections = await managementMenu.getSections();
|
||||
expect(sections).to.have.length(1);
|
||||
expect(sections[0]).to.eql({
|
||||
sectionId: 'kibana',
|
||||
sectionLinks: ['search_sessions'],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue