Skip to content

Commit

Permalink
[Data/Search Sessions] Management UI (#81707)
Browse files Browse the repository at this point in the history
* logging and error handling in session client routes

* [Data] Background Search Session Management UI

* functional tests

* fix ci

* new functional tests

* fix fn tests

* cleanups

* cleanup

* restore test

* configurable refresh and fetch timeout

* more tests

* feedback items

* take expiresSoon field out of the interface

* move helper to common

* remove bg sessions w/find and delete

* add storybook

* fix tests

* storybook actions

* refactor expiration status calculation

* isViewable as calculated field

* refreshInterval 10s default

* list newest first

* "Type" => "App"

* remove inline view action

* in-progress status tooltip shows expire date

* move date_string to public

* fix tests

* Adds management to tsconfig refs

* removes preemptive script fix

* view action was removed

* rename the feature to Search Sessions

* Update x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.tsx

Co-authored-by: Liza Katz <[email protected]>

* Update x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.tsx

Co-authored-by: Liza Katz <[email protected]>

* add TODO

* use RedirectAppLinks

* code review and react improvements

* config

* fix test

* Fix merge

* Fix management test

* @Dosant code review

* code review

* Deleteed story

* some more code review stuffs

* fix ts

* Code review and cleanup

* Added functional tests for restoring, reloading and canceling a dashboard
Renamed search session test service

* Don't show expiration time for canceled, expired or errored sessions

* fix jest

* Moved UISession to public

* @tsullivan code review

* Fixed import

Co-authored-by: Kibana Machine <[email protected]>
Co-authored-by: Christiane Heiligers <[email protected]>
Co-authored-by: Liza Katz <[email protected]>
Co-authored-by: Liza K <[email protected]>
  • Loading branch information
5 people authored Jan 20, 2021
1 parent c702b5c commit ef40806
Show file tree
Hide file tree
Showing 54 changed files with 5,448 additions and 60 deletions.
4 changes: 2 additions & 2 deletions src/plugins/data/public/search/session/sessions_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ export class SessionsClient {
});
}

public find(options: SavedObjectsFindOptions): Promise<SavedObjectsFindResponse> {
return this.http!.post(`/internal/session`, {
public find(options: Omit<SavedObjectsFindOptions, 'type'>): Promise<SavedObjectsFindResponse> {
return this.http!.post(`/internal/session/_find`, {
body: JSON.stringify(options),
});
}
Expand Down
5 changes: 3 additions & 2 deletions x-pack/plugins/data_enhanced/common/search/session/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { SearchSessionStatus } from './';

export interface SearchSessionSavedObjectAttributes {
/**
* User-facing session name to be displayed in session management
Expand All @@ -24,7 +26,7 @@ export interface SearchSessionSavedObjectAttributes {
/**
* status
*/
status: string;
status: SearchSessionStatus;
/**
* urlGeneratorId
*/
Expand All @@ -44,7 +46,6 @@ export interface SearchSessionSavedObjectAttributes {
*/
idMapping: Record<string, SearchSessionRequestInfo>;
}

export interface SearchSessionRequestInfo {
/**
* ID of the async search request
Expand Down
6 changes: 6 additions & 0 deletions x-pack/plugins/data_enhanced/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ export const configSchema = schema.object({
inMemTimeout: schema.duration({ defaultValue: '1m' }),
maxUpdateRetries: schema.number({ defaultValue: 3 }),
defaultExpiration: schema.duration({ defaultValue: '7d' }),
management: schema.object({
maxSessions: schema.number({ defaultValue: 10000 }),
refreshInterval: schema.duration({ defaultValue: '10s' }),
refreshTimeout: schema.duration({ defaultValue: '1m' }),
expiresSoonWarning: schema.duration({ defaultValue: '1d' }),
}),
}),
}),
});
Expand Down
11 changes: 2 additions & 9 deletions x-pack/plugins/data_enhanced/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,8 @@
"id": "dataEnhanced",
"version": "8.0.0",
"kibanaVersion": "kibana",
"configPath": [
"xpack", "data_enhanced"
],
"requiredPlugins": [
"bfetch",
"data",
"features",
"taskManager"
],
"configPath": ["xpack", "data_enhanced"],
"requiredPlugins": ["bfetch", "data", "features", "management", "share", "taskManager"],
"optionalPlugins": ["kibanaUtils", "usageCollection"],
"server": true,
"ui": true,
Expand Down
16 changes: 14 additions & 2 deletions x-pack/plugins/data_enhanced/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,25 @@ import React from 'react';
import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public';
import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public';
import { BfetchPublicSetup } from '../../../../src/plugins/bfetch/public';
import { ManagementSetup } from '../../../../src/plugins/management/public';
import { SharePluginStart } from '../../../../src/plugins/share/public';

import { setAutocompleteService } from './services';
import { setupKqlQuerySuggestionProvider, KUERY_LANGUAGE_NAME } from './autocomplete';
import { EnhancedSearchInterceptor } from './search/search_interceptor';
import { registerSearchSessionsMgmt } from './search/sessions_mgmt';
import { toMountPoint } from '../../../../src/plugins/kibana_react/public';
import { createConnectedSearchSessionIndicator } from './search';
import { ConfigSchema } from '../config';

export interface DataEnhancedSetupDependencies {
bfetch: BfetchPublicSetup;
data: DataPublicPluginSetup;
management: ManagementSetup;
}
export interface DataEnhancedStartDependencies {
data: DataPublicPluginStart;
share: SharePluginStart;
}

export type DataEnhancedSetup = ReturnType<DataEnhancedPlugin['setup']>;
Expand All @@ -30,12 +35,13 @@ export type DataEnhancedStart = ReturnType<DataEnhancedPlugin['start']>;
export class DataEnhancedPlugin
implements Plugin<void, void, DataEnhancedSetupDependencies, DataEnhancedStartDependencies> {
private enhancedSearchInterceptor!: EnhancedSearchInterceptor;
private config!: ConfigSchema;

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

public setup(
core: CoreSetup<DataEnhancedStartDependencies>,
{ bfetch, data }: DataEnhancedSetupDependencies
{ bfetch, data, management }: DataEnhancedSetupDependencies
) {
data.autocomplete.addQuerySuggestionProvider(
KUERY_LANGUAGE_NAME,
Expand All @@ -57,12 +63,18 @@ export class DataEnhancedPlugin
searchInterceptor: this.enhancedSearchInterceptor,
},
});

this.config = this.initializerContext.config.get<ConfigSchema>();
if (this.config.search.sessions.enabled) {
const { management: sessionsMgmtConfig } = this.config.search.sessions;
registerSearchSessionsMgmt(core, sessionsMgmtConfig, { management });
}
}

public start(core: CoreStart, plugins: DataEnhancedStartDependencies) {
setAutocompleteService(plugins.data.autocomplete);

if (this.initializerContext.config.get().search.sessions.enabled) {
if (this.config.search.sessions.enabled) {
core.chrome.setBreadcrumbsAppendExtension({
content: toMountPoint(
React.createElement(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React, { ReactNode } from 'react';
import { IntlProvider } from 'react-intl';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { UrlGeneratorsStart } from '../../../../../../../src/plugins/share/public/url_generators';

export function LocaleWrapper({ children }: { children?: ReactNode }) {
return <IntlProvider locale="en">{children}</IntlProvider>;
}

export const mockUrls = ({
getUrlGenerator: (id: string) => ({ createUrl: () => `hello-cool-${id}-url` }),
} as unknown) as UrlGeneratorsStart;
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { CoreSetup } from 'kibana/public';
import type { ManagementAppMountParams } from 'src/plugins/management/public';
import type {
AppDependencies,
IManagementSectionsPluginsSetup,
IManagementSectionsPluginsStart,
SessionsMgmtConfigSchema,
} from '../';
import { APP } from '../';
import { SearchSessionsMgmtAPI } from '../lib/api';
import { AsyncSearchIntroDocumentation } from '../lib/documentation';
import { renderApp } from './render';

export class SearchSessionsMgmtApp {
constructor(
private coreSetup: CoreSetup<IManagementSectionsPluginsStart>,
private config: SessionsMgmtConfigSchema,
private params: ManagementAppMountParams,
private pluginsSetup: IManagementSectionsPluginsSetup
) {}

public async mountManagementSection() {
const { coreSetup, params, pluginsSetup } = this;
const [coreStart, pluginsStart] = await coreSetup.getStartServices();

const {
chrome: { docTitle },
http,
docLinks,
i18n,
notifications,
uiSettings,
application,
} = coreStart;
const { data, share } = pluginsStart;

const pluginName = APP.getI18nName();
docTitle.change(pluginName);
params.setBreadcrumbs([{ text: pluginName }]);

const { sessionsClient } = data.search;
const api = new SearchSessionsMgmtAPI(sessionsClient, this.config, {
notifications,
urls: share.urlGenerators,
application,
});

const documentation = new AsyncSearchIntroDocumentation(docLinks);

const dependencies: AppDependencies = {
plugins: pluginsSetup,
config: this.config,
documentation,
core: coreStart,
api,
http,
i18n,
uiSettings,
share,
};

const { element } = params;
const unmountAppCb = renderApp(element, dependencies);

return () => {
docTitle.reset();
unmountAppCb();
};
}
}

export { renderApp };
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { AppDependencies } from '../';
import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public';
import { SearchSessionsMgmtMain } from '../components/main';

export const renderApp = (
elem: HTMLElement | null,
{ i18n, uiSettings, ...homeDeps }: AppDependencies
) => {
if (!elem) {
return () => undefined;
}

const { Context: I18nContext } = i18n;
// uiSettings is required by the listing table to format dates in the timezone from Settings
const { Provider: KibanaReactContextProvider } = createKibanaReactContext({
uiSettings,
});

render(
<I18nContext>
<KibanaReactContextProvider>
<SearchSessionsMgmtMain {...homeDeps} timezone={uiSettings.get('dateFormat:tz')} />
</KibanaReactContextProvider>
</I18nContext>,
elem
);

return () => {
unmountComponentAtNode(elem);
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { useState } from 'react';
import { SearchSessionsMgmtAPI } from '../../lib/api';
import { TableText } from '../';
import { OnActionComplete } from './types';

interface CancelButtonProps {
id: string;
name: string;
api: SearchSessionsMgmtAPI;
onActionComplete: OnActionComplete;
}

const CancelConfirm = ({
onConfirmDismiss,
...props
}: CancelButtonProps & { onConfirmDismiss: () => void }) => {
const { id, name, api, onActionComplete } = props;
const [isLoading, setIsLoading] = useState(false);

const title = i18n.translate('xpack.data.mgmt.searchSessions.cancelModal.title', {
defaultMessage: 'Cancel search session',
});
const confirm = i18n.translate('xpack.data.mgmt.searchSessions.cancelModal.cancelButton', {
defaultMessage: 'Cancel',
});
const cancel = i18n.translate('xpack.data.mgmt.searchSessions.cancelModal.dontCancelButton', {
defaultMessage: 'Dismiss',
});
const message = i18n.translate('xpack.data.mgmt.searchSessions.cancelModal.message', {
defaultMessage: `Canceling the search session \'{name}\' will expire any cached results, so that quick restore will no longer be available. You will still be able to re-run it, using the reload action.`,
values: {
name,
},
});

return (
<EuiOverlayMask>
<EuiConfirmModal
title={title}
onCancel={onConfirmDismiss}
onConfirm={async () => {
setIsLoading(true);
await api.sendCancel(id);
onActionComplete();
}}
confirmButtonText={confirm}
confirmButtonDisabled={isLoading}
cancelButtonText={cancel}
defaultFocusedButton="confirm"
buttonColor="danger"
>
{message}
</EuiConfirmModal>
</EuiOverlayMask>
);
};

export const CancelButton = (props: CancelButtonProps) => {
const [showConfirm, setShowConfirm] = useState(false);

const onClick = () => {
setShowConfirm(true);
};

const onConfirmDismiss = () => {
setShowConfirm(false);
};

return (
<>
<TableText onClick={onClick}>
<FormattedMessage
id="xpack.data.mgmt.searchSessions.actionCancel"
defaultMessage="Cancel"
/>
</TableText>
{showConfirm ? <CancelConfirm {...props} onConfirmDismiss={onConfirmDismiss} /> : null}
</>
);
};
Loading

0 comments on commit ef40806

Please sign in to comment.