Improve extend search session UI (#89126)

* Fix extend search session UI

* code review
This commit is contained in:
Liza Katz 2021-01-26 13:08:03 +02:00 committed by GitHub
parent 1994c5bfd9
commit de44af3eeb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 110 additions and 66 deletions

View file

@ -66,8 +66,8 @@ export class DataEnhancedPlugin
this.config = this.initializerContext.config.get<ConfigSchema>();
if (this.config.search.sessions.enabled) {
const { management: sessionsMgmtConfig } = this.config.search.sessions;
registerSearchSessionsMgmt(core, sessionsMgmtConfig, { management });
const sessionsConfig = this.config.search.sessions;
registerSearchSessionsMgmt(core, sessionsConfig, { management });
}
}

View file

@ -10,7 +10,7 @@ import type {
AppDependencies,
IManagementSectionsPluginsSetup,
IManagementSectionsPluginsStart,
SessionsMgmtConfigSchema,
SessionsConfigSchema,
} from '../';
import { APP } from '../';
import { SearchSessionsMgmtAPI } from '../lib/api';
@ -20,7 +20,7 @@ import { renderApp } from './render';
export class SearchSessionsMgmtApp {
constructor(
private coreSetup: CoreSetup<IManagementSectionsPluginsStart>,
private config: SessionsMgmtConfigSchema,
private config: SessionsConfigSchema,
private params: ManagementAppMountParams,
private pluginsSetup: IManagementSectionsPluginsSetup
) {}

View file

@ -8,6 +8,8 @@ import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { useState } from 'react';
import { Duration } from 'moment';
import moment from 'moment';
import { SearchSessionsMgmtAPI } from '../../lib/api';
import { TableText } from '../';
import { OnActionComplete } from './types';
@ -15,6 +17,8 @@ import { OnActionComplete } from './types';
interface ExtendButtonProps {
id: string;
name: string;
expires: string | null;
extendBy: Duration;
api: SearchSessionsMgmtAPI;
onActionComplete: OnActionComplete;
}
@ -23,8 +27,11 @@ const ExtendConfirm = ({
onConfirmDismiss,
...props
}: ExtendButtonProps & { onConfirmDismiss: () => void }) => {
const { id, name, api, onActionComplete } = props;
const { id, name, expires, api, extendBy, onActionComplete } = props;
const [isLoading, setIsLoading] = useState(false);
const extendByDuration = moment.duration(extendBy);
const newExpiration = moment(expires).add(extendByDuration);
const title = i18n.translate('xpack.data.mgmt.searchSessions.extendModal.title', {
defaultMessage: 'Extend search session expiration',
@ -36,9 +43,10 @@ const ExtendConfirm = ({
defaultMessage: 'Cancel',
});
const message = i18n.translate('xpack.data.mgmt.searchSessions.extendModal.extendMessage', {
defaultMessage: "When would you like the search session '{name}' to expire?",
defaultMessage: "The search session '{name}' expiration would be extended until {newExpires}.",
values: {
name,
newExpires: newExpiration.toLocaleString(),
},
});
@ -49,7 +57,7 @@ const ExtendConfirm = ({
onCancel={onConfirmDismiss}
onConfirm={async () => {
setIsLoading(true);
await api.sendExtend(id, '1');
await api.sendExtend(id, `${extendByDuration.asMilliseconds()}ms`);
onActionComplete();
}}
confirmButtonText={confirm}

View file

@ -17,7 +17,7 @@ import { ACTION, OnActionComplete } from './types';
export const getAction = (
api: SearchSessionsMgmtAPI,
actionType: string,
{ id, name, reloadUrl }: UISession,
{ id, name, expires, reloadUrl }: UISession,
onActionComplete: OnActionComplete
): IClickActionDescriptor | null => {
switch (actionType) {
@ -39,7 +39,16 @@ export const getAction = (
return {
iconType: extendSessionIcon,
textColor: 'default',
label: <ExtendButton api={api} id={id} name={name} onActionComplete={onActionComplete} />,
label: (
<ExtendButton
api={api}
id={id}
name={name}
expires={expires}
extendBy={api.getExtendByDuration()}
onActionComplete={onActionComplete}
/>
),
};
default:

View file

@ -12,7 +12,7 @@ import React from 'react';
import { act } from 'react-dom/test-utils';
import { coreMock } from 'src/core/public/mocks';
import { SessionsClient } from 'src/plugins/data/public/search';
import { SessionsMgmtConfigSchema } from '..';
import { SessionsConfigSchema } from '..';
import { SearchSessionsMgmtAPI } from '../lib/api';
import { AsyncSearchIntroDocumentation } from '../lib/documentation';
import { LocaleWrapper, mockUrls } from '../__mocks__';
@ -20,7 +20,7 @@ import { SearchSessionsMgmtMain } from './main';
let mockCoreSetup: MockedKeys<CoreSetup>;
let mockCoreStart: MockedKeys<CoreStart>;
let mockConfig: SessionsMgmtConfigSchema;
let mockConfig: SessionsConfigSchema;
let sessionsClient: SessionsClient;
let api: SearchSessionsMgmtAPI;
@ -29,11 +29,14 @@ describe('Background Search Session Management Main', () => {
mockCoreSetup = coreMock.createSetup();
mockCoreStart = coreMock.createStart();
mockConfig = {
expiresSoonWarning: moment.duration(1, 'days'),
maxSessions: 2000,
refreshInterval: moment.duration(1, 'seconds'),
refreshTimeout: moment.duration(10, 'minutes'),
};
defaultExpiration: moment.duration('7d'),
management: {
expiresSoonWarning: moment.duration(1, 'days'),
maxSessions: 2000,
refreshInterval: moment.duration(1, 'seconds'),
refreshTimeout: moment.duration(10, 'minutes'),
},
} as any;
sessionsClient = new SessionsClient({ http: mockCoreSetup.http });

View file

@ -17,7 +17,7 @@ import {
import { FormattedMessage } from '@kbn/i18n/react';
import type { CoreStart, HttpStart } from 'kibana/public';
import React from 'react';
import type { SessionsMgmtConfigSchema } from '../';
import type { SessionsConfigSchema } from '../';
import type { SearchSessionsMgmtAPI } from '../lib/api';
import type { AsyncSearchIntroDocumentation } from '../lib/documentation';
import { TableText } from './';
@ -29,7 +29,7 @@ interface Props {
api: SearchSessionsMgmtAPI;
http: HttpStart;
timezone: string;
config: SessionsMgmtConfigSchema;
config: SessionsConfigSchema;
}
export function SearchSessionsMgmtMain({ documentation, ...tableProps }: Props) {

View file

@ -13,14 +13,14 @@ import React from 'react';
import { coreMock } from 'src/core/public/mocks';
import { SessionsClient } from 'src/plugins/data/public/search';
import { SearchSessionStatus } from '../../../../../common/search';
import { SessionsMgmtConfigSchema } from '../../';
import { SessionsConfigSchema } from '../../';
import { SearchSessionsMgmtAPI } from '../../lib/api';
import { LocaleWrapper, mockUrls } from '../../__mocks__';
import { SearchSessionsMgmtTable } from './table';
let mockCoreSetup: MockedKeys<CoreSetup>;
let mockCoreStart: CoreStart;
let mockConfig: SessionsMgmtConfigSchema;
let mockConfig: SessionsConfigSchema;
let sessionsClient: SessionsClient;
let api: SearchSessionsMgmtAPI;
@ -29,11 +29,14 @@ describe('Background Search Session Management Table', () => {
mockCoreSetup = coreMock.createSetup();
mockCoreStart = coreMock.createStart();
mockConfig = {
expiresSoonWarning: moment.duration(1, 'days'),
maxSessions: 2000,
refreshInterval: moment.duration(1, 'seconds'),
refreshTimeout: moment.duration(10, 'minutes'),
};
defaultExpiration: moment.duration('7d'),
management: {
expiresSoonWarning: moment.duration(1, 'days'),
maxSessions: 2000,
refreshInterval: moment.duration(1, 'seconds'),
refreshTimeout: moment.duration(10, 'minutes'),
},
} as any;
sessionsClient = new SessionsClient({ http: mockCoreSetup.http });
api = new SearchSessionsMgmtAPI(sessionsClient, mockConfig, {
@ -134,7 +137,10 @@ describe('Background Search Session Management Table', () => {
sessionsClient.find = jest.fn();
mockConfig = {
...mockConfig,
refreshInterval: moment.duration(10, 'seconds'),
management: {
...mockConfig.management,
refreshInterval: moment.duration(10, 'seconds'),
},
};
await act(async () => {
@ -162,8 +168,11 @@ describe('Background Search Session Management Table', () => {
mockConfig = {
...mockConfig,
refreshInterval: moment.duration(1, 'day'),
refreshTimeout: moment.duration(2, 'days'),
management: {
...mockConfig.management,
refreshInterval: moment.duration(1, 'day'),
refreshTimeout: moment.duration(2, 'days'),
},
};
await act(async () => {

View file

@ -12,7 +12,7 @@ import React, { useCallback, useMemo, useRef, useEffect, useState } from 'react'
import useDebounce from 'react-use/lib/useDebounce';
import useInterval from 'react-use/lib/useInterval';
import { TableText } from '../';
import { SessionsMgmtConfigSchema } from '../..';
import { SessionsConfigSchema } from '../..';
import { SearchSessionsMgmtAPI } from '../../lib/api';
import { getColumns } from '../../lib/get_columns';
import { UISession } from '../../types';
@ -26,7 +26,7 @@ interface Props {
core: CoreStart;
api: SearchSessionsMgmtAPI;
timezone: string;
config: SessionsMgmtConfigSchema;
config: SessionsConfigSchema;
}
export function SearchSessionsMgmtTable({ core, api, timezone, config, ...props }: Props) {
@ -35,9 +35,10 @@ export function SearchSessionsMgmtTable({ core, api, timezone, config, ...props
const [debouncedIsLoading, setDebouncedIsLoading] = useState(false);
const [pagination, setPagination] = useState({ pageIndex: 0 });
const showLatestResultsHandler = useRef<Function>();
const refreshInterval = useMemo(() => moment.duration(config.refreshInterval).asMilliseconds(), [
config.refreshInterval,
]);
const refreshInterval = useMemo(
() => moment.duration(config.management.refreshInterval).asMilliseconds(),
[config.management.refreshInterval]
);
// Debounce rendering the state of the Refresh button
useDebounce(

View file

@ -33,7 +33,7 @@ export interface AppDependencies {
api: SearchSessionsMgmtAPI;
http: HttpStart;
i18n: I18nStart;
config: SessionsMgmtConfigSchema;
config: SessionsConfigSchema;
}
export const APP = {
@ -44,11 +44,11 @@ export const APP = {
}),
};
export type SessionsMgmtConfigSchema = ConfigSchema['search']['sessions']['management'];
export type SessionsConfigSchema = ConfigSchema['search']['sessions'];
export function registerSearchSessionsMgmt(
coreSetup: CoreSetup<DataEnhancedStartDependencies>,
config: SessionsMgmtConfigSchema,
config: SessionsConfigSchema,
services: IManagementSectionsPluginsSetup
) {
services.management.sections.section.kibana.registerApp({

View file

@ -11,14 +11,14 @@ import { coreMock } from 'src/core/public/mocks';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import type { SavedObjectsFindResponse } from 'src/core/server';
import { SessionsClient } from 'src/plugins/data/public/search';
import type { SessionsMgmtConfigSchema } from '../';
import type { SessionsConfigSchema } from '../';
import { SearchSessionStatus } from '../../../../common/search';
import { mockUrls } from '../__mocks__';
import { SearchSessionsMgmtAPI } from './api';
let mockCoreSetup: MockedKeys<CoreSetup>;
let mockCoreStart: MockedKeys<CoreStart>;
let mockConfig: SessionsMgmtConfigSchema;
let mockConfig: SessionsConfigSchema;
let sessionsClient: SessionsClient;
describe('Search Sessions Management API', () => {
@ -26,11 +26,14 @@ describe('Search Sessions Management API', () => {
mockCoreSetup = coreMock.createSetup();
mockCoreStart = coreMock.createStart();
mockConfig = {
expiresSoonWarning: moment.duration('1d'),
maxSessions: 2000,
refreshInterval: moment.duration('1s'),
refreshTimeout: moment.duration('10m'),
};
defaultExpiration: moment.duration('7d'),
management: {
expiresSoonWarning: moment.duration(1, 'days'),
maxSessions: 2000,
refreshInterval: moment.duration(1, 'seconds'),
refreshTimeout: moment.duration(10, 'minutes'),
},
} as any;
sessionsClient = new SessionsClient({ http: mockCoreSetup.http });
});
@ -93,8 +96,11 @@ describe('Search Sessions Management API', () => {
test('handle timeout error', async () => {
mockConfig = {
...mockConfig,
refreshInterval: moment.duration(1, 'hours'),
refreshTimeout: moment.duration(1, 'seconds'),
management: {
...mockConfig.management,
refreshInterval: moment.duration(1, 'hours'),
refreshTimeout: moment.duration(1, 'seconds'),
},
};
sessionsClient.find = jest.fn().mockImplementation(async () => {

View file

@ -10,7 +10,7 @@ import moment from 'moment';
import { from, race, timer } from 'rxjs';
import { mapTo, tap } from 'rxjs/operators';
import type { SharePluginStart } from 'src/plugins/share/public';
import { SessionsMgmtConfigSchema } from '../';
import { SessionsConfigSchema } from '../';
import type { ISessionsClient } from '../../../../../../../src/plugins/data/public';
import type { SearchSessionSavedObjectAttributes } from '../../../../common';
import { SearchSessionStatus } from '../../../../common/search';
@ -47,10 +47,9 @@ async function getUrlFromState(
}
// Helper: factory for a function to map server objects to UI objects
const mapToUISession = (
urls: UrlGeneratorsStart,
{ expiresSoonWarning }: SessionsMgmtConfigSchema
) => async (savedObject: SavedObject<SearchSessionSavedObjectAttributes>): Promise<UISession> => {
const mapToUISession = (urls: UrlGeneratorsStart, config: SessionsConfigSchema) => async (
savedObject: SavedObject<SearchSessionSavedObjectAttributes>
): Promise<UISession> => {
const {
name,
appId,
@ -92,7 +91,7 @@ interface SearcgSessuibManagementDeps {
export class SearchSessionsMgmtAPI {
constructor(
private sessionsClient: ISessionsClient,
private config: SessionsMgmtConfigSchema,
private config: SessionsConfigSchema,
private deps: SearcgSessuibManagementDeps
) {}
@ -101,12 +100,14 @@ export class SearchSessionsMgmtAPI {
saved_objects: object[];
}
const refreshTimeout = moment.duration(this.config.refreshTimeout);
const mgmtConfig = this.config.management;
const refreshTimeout = moment.duration(mgmtConfig.refreshTimeout);
const fetch$ = from(
this.sessionsClient.find({
page: 1,
perPage: this.config.maxSessions,
perPage: mgmtConfig.maxSessions,
sortField: 'created',
sortOrder: 'asc',
})
@ -149,6 +150,10 @@ export class SearchSessionsMgmtAPI {
this.deps.application.navigateToUrl(reloadUrl);
}
public getExtendByDuration() {
return this.config.defaultExpiration;
}
// Cancel and expire
public async sendCancel(id: string): Promise<void> {
try {

View file

@ -12,7 +12,7 @@ import moment from 'moment';
import { ReactElement } from 'react';
import { coreMock } from 'src/core/public/mocks';
import { SessionsClient } from 'src/plugins/data/public/search';
import { SessionsMgmtConfigSchema } from '../';
import { SessionsConfigSchema } from '../';
import { SearchSessionStatus } from '../../../../common/search';
import { OnActionComplete } from '../components';
import { UISession } from '../types';
@ -22,7 +22,7 @@ import { getColumns } from './get_columns';
let mockCoreSetup: MockedKeys<CoreSetup>;
let mockCoreStart: CoreStart;
let mockConfig: SessionsMgmtConfigSchema;
let mockConfig: SessionsConfigSchema;
let api: SearchSessionsMgmtAPI;
let sessionsClient: SessionsClient;
let handleAction: OnActionComplete;
@ -35,11 +35,14 @@ describe('Search Sessions Management table column factory', () => {
mockCoreSetup = coreMock.createSetup();
mockCoreStart = coreMock.createStart();
mockConfig = {
expiresSoonWarning: moment.duration(1, 'days'),
maxSessions: 2000,
refreshInterval: moment.duration(1, 'seconds'),
refreshTimeout: moment.duration(10, 'minutes'),
};
defaultExpiration: moment.duration('7d'),
management: {
expiresSoonWarning: moment.duration(1, 'days'),
maxSessions: 2000,
refreshInterval: moment.duration(1, 'seconds'),
refreshTimeout: moment.duration(10, 'minutes'),
},
} as any;
sessionsClient = new SessionsClient({ http: mockCoreSetup.http });
api = new SearchSessionsMgmtAPI(sessionsClient, mockConfig, {

View file

@ -20,7 +20,7 @@ import { capitalize } from 'lodash';
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { RedirectAppLinks } from '../../../../../../../src/plugins/kibana_react/public';
import { SessionsMgmtConfigSchema } from '../';
import { SessionsConfigSchema } from '../';
import { SearchSessionStatus } from '../../../../common/search';
import { TableText } from '../components';
import { OnActionComplete, PopoverActionsMenu } from '../components';
@ -45,7 +45,7 @@ function isSessionRestorable(status: SearchSessionStatus) {
export const getColumns = (
core: CoreStart,
api: SearchSessionsMgmtAPI,
config: SessionsMgmtConfigSchema,
config: SessionsConfigSchema,
timezone: string,
onActionComplete: OnActionComplete
): Array<EuiBasicTableColumn<UISession>> => {

View file

@ -6,9 +6,9 @@
import { i18n } from '@kbn/i18n';
import moment from 'moment';
import { SessionsMgmtConfigSchema } from '../';
import { SessionsConfigSchema } from '../';
export const getExpirationStatus = (config: SessionsMgmtConfigSchema, expires: string | null) => {
export const getExpirationStatus = (config: SessionsConfigSchema, expires: string | null) => {
const tNow = moment.utc().valueOf();
const tFuture = moment.utc(expires).valueOf();
@ -16,7 +16,7 @@ export const getExpirationStatus = (config: SessionsMgmtConfigSchema, expires: s
// and the session was early expired when the browser refreshed the listing
const durationToExpire = moment.duration(tFuture - tNow);
const expiresInDays = Math.floor(durationToExpire.asDays());
const sufficientDays = Math.ceil(moment.duration(config.expiresSoonWarning).asDays());
const sufficientDays = Math.ceil(moment.duration(config.management.expiresSoonWarning).asDays());
let toolTipContent = i18n.translate('xpack.data.mgmt.searchSessions.status.expiresSoonInDays', {
defaultMessage: 'Expires in {numDays} days',

View file

@ -76,7 +76,7 @@ export async function scheduleSearchSessionsTasks(
params: {},
});
logger.debug(`Background search task, scheduled to run`);
logger.debug(`Search sessions task, scheduled to run`);
} catch (e) {
logger.debug(`Error scheduling task, received ${e.message}`);
}